Merge branch 'misskey-dev:develop' into develop
This commit is contained in:
commit
afc6820ef7
@ -95,6 +95,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
@ -105,6 +105,16 @@ redis:
|
|||||||
# # You can specify more ioredis options...
|
# # You can specify more ioredis options...
|
||||||
# #username: example-username
|
# #username: example-username
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
# # You can specify more ioredis options...
|
||||||
|
# #username: example-username
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
@ -95,6 +95,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
21
CHANGELOG.md
21
CHANGELOG.md
@ -12,6 +12,27 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 2023.10.0
|
||||||
|
### NOTE
|
||||||
|
- muted_noteテーブルは使われなくなったため手動で削除を行ってください。
|
||||||
|
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
|
||||||
|
- API: notes/global-timeline は現在常に `[]` を返します
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
|
||||||
|
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
|
||||||
|
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
|
||||||
|
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: タイムライン取得時のパフォーマンスを改善
|
||||||
|
|
||||||
## 2023.9.3
|
## 2023.9.3
|
||||||
### General
|
### General
|
||||||
- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に
|
- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に
|
||||||
|
@ -116,6 +116,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
9
locales/index.d.ts
vendored
9
locales/index.d.ts
vendored
@ -1129,6 +1129,9 @@ export interface Locale {
|
|||||||
"notificationRecieveConfig": string;
|
"notificationRecieveConfig": string;
|
||||||
"mutualFollow": string;
|
"mutualFollow": string;
|
||||||
"fileAttachedOnly": string;
|
"fileAttachedOnly": string;
|
||||||
|
"showRepliesToOthersInTimeline": string;
|
||||||
|
"hideRepliesToOthersInTimeline": string;
|
||||||
|
"externalServices": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
@ -1542,7 +1545,6 @@ export interface Locale {
|
|||||||
"gtlAvailable": string;
|
"gtlAvailable": string;
|
||||||
"ltlAvailable": string;
|
"ltlAvailable": string;
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
"canEditNote": string;
|
|
||||||
"canInvite": string;
|
"canInvite": string;
|
||||||
"inviteLimit": string;
|
"inviteLimit": string;
|
||||||
"inviteLimitCycle": string;
|
"inviteLimitCycle": string;
|
||||||
@ -1719,11 +1721,6 @@ export interface Locale {
|
|||||||
"muteWords": string;
|
"muteWords": string;
|
||||||
"muteWordsDescription": string;
|
"muteWordsDescription": string;
|
||||||
"muteWordsDescription2": string;
|
"muteWordsDescription2": string;
|
||||||
"softDescription": string;
|
|
||||||
"hardDescription": string;
|
|
||||||
"soft": string;
|
|
||||||
"hard": string;
|
|
||||||
"mutedNotes": string;
|
|
||||||
};
|
};
|
||||||
"_instanceMute": {
|
"_instanceMute": {
|
||||||
"instanceMuteDescription": string;
|
"instanceMuteDescription": string;
|
||||||
|
@ -1126,6 +1126,9 @@ edited: "編集済み"
|
|||||||
notificationRecieveConfig: "通知の受信設定"
|
notificationRecieveConfig: "通知の受信設定"
|
||||||
mutualFollow: "相互フォロー"
|
mutualFollow: "相互フォロー"
|
||||||
fileAttachedOnly: "ファイル付きのみ"
|
fileAttachedOnly: "ファイル付きのみ"
|
||||||
|
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
|
||||||
|
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
|
||||||
|
externalServices: "外部サービス"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
@ -1463,7 +1466,6 @@ _role:
|
|||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
canEditNote: "ノートの編集"
|
|
||||||
canInvite: "サーバー招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
inviteLimit: "招待コードの作成可能数"
|
inviteLimit: "招待コードの作成可能数"
|
||||||
inviteLimitCycle: "招待コードの発行間隔"
|
inviteLimitCycle: "招待コードの発行間隔"
|
||||||
@ -1636,11 +1638,6 @@ _wordMute:
|
|||||||
muteWords: "ミュートするワード"
|
muteWords: "ミュートするワード"
|
||||||
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
||||||
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
softDescription: "指定した条件のノートをタイムラインから隠します。"
|
|
||||||
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
|
|
||||||
soft: "ソフト"
|
|
||||||
hard: "ハード"
|
|
||||||
mutedNotes: "ミュートされたノート"
|
|
||||||
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。"
|
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.9.3",
|
"version": "2023.10.0-beta.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -216,4 +216,6 @@ module.exports = {
|
|||||||
maxWorkers: 1, // Make it use worker (that can be killed and restarted)
|
maxWorkers: 1, // Make it use worker (that can be killed and restarted)
|
||||||
logHeapUsage: true, // To debug when out-of-memory happens on CI
|
logHeapUsage: true, // To debug when out-of-memory happens on CI
|
||||||
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
|
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
|
||||||
|
|
||||||
|
maxConcurrency: 32,
|
||||||
};
|
};
|
||||||
|
20
packages/backend/migration/1696222183852-withReplies.js
Normal file
20
packages/backend/migration/1696222183852-withReplies.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class WithReplies1696222183852 {
|
||||||
|
name = 'WithReplies1696222183852'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export class UserListMembership1696323464251 {
|
||||||
|
name = 'UserListMembership1696323464251'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`);
|
||||||
|
}
|
||||||
|
}
|
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export class Hibernation1696331570827 {
|
||||||
|
name = 'Hibernation1696331570827'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||||
|
}
|
||||||
|
}
|
33
packages/backend/migration/1696332072038-clean.js
Normal file
33
packages/backend/migration/1696332072038-clean.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export class Clean1696332072038 {
|
||||||
|
name = 'Clean1696332072038'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class MetaCacheSettings1696373953614 {
|
||||||
|
name = 'MetaCacheSettings1696373953614'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`);
|
||||||
|
}
|
||||||
|
}
|
16
packages/backend/migration/1696388600237-revert-note-edit.js
Normal file
16
packages/backend/migration/1696388600237-revert-note-edit.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class RevertNoteEdit1696388600237 {
|
||||||
|
name = 'RevertNoteEdit1696388600237'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
}
|
@ -70,11 +70,19 @@ const $redisForSub: Provider = {
|
|||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $redisForTimelines: Provider = {
|
||||||
|
provide: DI.redisForTimelines,
|
||||||
|
useFactory: (config: Config) => {
|
||||||
|
return new Redis.Redis(config.redisForTimelines);
|
||||||
|
},
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RepositoryModule],
|
imports: [RepositoryModule],
|
||||||
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
|
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
|
||||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
|
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
|
||||||
})
|
})
|
||||||
export class GlobalModule implements OnApplicationShutdown {
|
export class GlobalModule implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
@ -82,6 +90,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redis) private redisClient: Redis.Redis,
|
@Inject(DI.redis) private redisClient: Redis.Redis,
|
||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
@ -98,6 +107,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
this.redisForPub.disconnect(),
|
this.redisForPub.disconnect(),
|
||||||
this.redisForSub.disconnect(),
|
this.redisForSub.disconnect(),
|
||||||
|
this.redisForTimelines.disconnect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ type Source = {
|
|||||||
redis: RedisOptionsSource;
|
redis: RedisOptionsSource;
|
||||||
redisForPubsub?: RedisOptionsSource;
|
redisForPubsub?: RedisOptionsSource;
|
||||||
redisForJobQueue?: RedisOptionsSource;
|
redisForJobQueue?: RedisOptionsSource;
|
||||||
|
redisForTimelines?: RedisOptionsSource;
|
||||||
meilisearch?: {
|
meilisearch?: {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
@ -161,6 +162,7 @@ export type Config = {
|
|||||||
redis: RedisOptions & RedisOptionsSource;
|
redis: RedisOptions & RedisOptionsSource;
|
||||||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||||
|
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||||
perChannelMaxNoteCacheCount: number;
|
perChannelMaxNoteCacheCount: number;
|
||||||
perUserNotificationsMaxCount: number;
|
perUserNotificationsMaxCount: number;
|
||||||
deactivateAntennaThreshold: number;
|
deactivateAntennaThreshold: number;
|
||||||
@ -227,6 +229,7 @@ export function loadConfig(): Config {
|
|||||||
redis,
|
redis,
|
||||||
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
||||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||||
|
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||||
id: config.id,
|
id: config.id,
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
proxySmtp: config.proxySmtp,
|
proxySmtp: config.proxySmtp,
|
||||||
|
@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
@ -42,8 +42,8 @@ export class AccountMoveService {
|
|||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
@ -215,40 +215,40 @@ export class AccountMoveService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
|
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
|
||||||
// Return if there is no list to be updated.
|
// Return if there is no list to be updated.
|
||||||
const oldJoinings = await this.userListJoiningsRepository.find({
|
const oldMemberships = await this.userListMembershipsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userId: src.id,
|
userId: src.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (oldJoinings.length === 0) return;
|
if (oldMemberships.length === 0) return;
|
||||||
|
|
||||||
const existingUserListIds = await this.userListJoiningsRepository.find({
|
const existingUserListIds = await this.userListMembershipsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userId: dst.id,
|
userId: dst.id,
|
||||||
},
|
},
|
||||||
}).then(joinings => joinings.map(joining => joining.userListId));
|
}).then(memberships => memberships.map(membership => membership.userListId));
|
||||||
|
|
||||||
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||||
|
|
||||||
// 重複しないようにIDを生成
|
// 重複しないようにIDを生成
|
||||||
const genId = (): string => {
|
const genId = (): string => {
|
||||||
let id: string;
|
let id: string;
|
||||||
do {
|
do {
|
||||||
id = this.idService.genId();
|
id = this.idService.genId();
|
||||||
} while (newJoinings.has(id));
|
} while (newMemberships.has(id));
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
for (const joining of oldJoinings) {
|
for (const membership of oldMemberships) {
|
||||||
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
|
if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list
|
||||||
newJoinings.set(genId(), {
|
newMemberships.set(genId(), {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: dst.id,
|
userId: dst.id,
|
||||||
userListId: joining.userListId,
|
userListId: membership.userListId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||||
await this.userListJoiningsRepository.insert(arrayToInsert);
|
await this.userListMembershipsRepository.insert(arrayToInsert);
|
||||||
|
|
||||||
// Have the proxy account follow the new account in the same way as UserListService.push
|
// Have the proxy account follow the new account in the same way as UserListService.push
|
||||||
if (this.userEntityService.isRemoteUser(dst)) {
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
|
@ -12,7 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.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 { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
@ -24,8 +24,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
private antennas: MiAntenna[];
|
private antennas: MiAntenna[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
@ -33,8 +33,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
@ -81,7 +81,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||||
|
|
||||||
const redisPipeline = this.redisClient.pipeline();
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
for (const antenna of matchedAntennas) {
|
for (const antenna of matchedAntennas) {
|
||||||
redisPipeline.xadd(
|
redisPipeline.xadd(
|
||||||
@ -108,7 +108,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||||
userListId: antenna.userListId!,
|
userListId: antenna.userListId!,
|
||||||
})).map(x => x.userId);
|
})).map(x => x.userId);
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
public userFollowingsCache: RedisKVCache<Set<string>>;
|
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||||
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -136,12 +136,18 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
|
this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
memoryCacheLifetime: 1000 * 60, // 1m
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => {
|
||||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
for (const x of xs) {
|
||||||
|
obj[x.followeeId] = { withReplies: x.withReplies };
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||||
@ -188,6 +194,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
if (follower) follower.followingCount++;
|
if (follower) follower.followingCount++;
|
||||||
const followee = this.userByIdCache.get(body.followeeId);
|
const followee = this.userByIdCache.get(body.followeeId);
|
||||||
if (followee) followee.followersCount++;
|
if (followee) followee.followersCount++;
|
||||||
|
this.userFollowingsCache.delete(body.followerId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js';
|
|||||||
import { WebAuthnService } from './WebAuthnService.js';
|
import { WebAuthnService } from './WebAuthnService.js';
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { CacheService } from './CacheService.js';
|
import { CacheService } from './CacheService.js';
|
||||||
|
import { UserService } from './UserService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairService } from './UserKeypairService.js';
|
import { UserKeypairService } from './UserKeypairService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
@ -173,6 +174,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
|
|||||||
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||||
|
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
@ -303,6 +305,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
@ -426,6 +429,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
@ -550,6 +554,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
@ -672,6 +677,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
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, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
|||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
@ -54,8 +54,6 @@ import { RoleService } from '@/core/RoleService.js';
|
|||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
|
|
||||||
const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
@ -157,8 +155,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
@ -175,8 +173,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
@ -187,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFollowingsRepository)
|
||||||
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@ -334,7 +335,7 @@ 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) {
|
if (data.channel) {
|
||||||
this.redisClient.xadd(
|
this.redisForTimelines.xadd(
|
||||||
`channelTimeline:${data.channel.id}`,
|
`channelTimeline:${data.channel.id}`,
|
||||||
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
|
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
|
||||||
'*',
|
'*',
|
||||||
@ -480,26 +481,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
this.incNotesCountOfUser(user);
|
this.incNotesCountOfUser(user);
|
||||||
|
|
||||||
// Word mute
|
if (data.visibility === 'specified') {
|
||||||
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
// TODO?
|
||||||
where: {
|
} else {
|
||||||
enableWordMute: true,
|
this.pushToTl(note, user);
|
||||||
},
|
}
|
||||||
select: ['userId', 'mutedWords'],
|
|
||||||
})).then(us => {
|
|
||||||
for (const u of us) {
|
|
||||||
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
|
|
||||||
if (shouldMute) {
|
|
||||||
this.mutedNotesRepository.insert({
|
|
||||||
id: this.idService.genId(),
|
|
||||||
userId: u.userId,
|
|
||||||
noteId: note.id,
|
|
||||||
reason: 'word',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.antennaService.addNoteToAntennas(note, user);
|
this.antennaService.addNoteToAntennas(note, user);
|
||||||
|
|
||||||
@ -508,11 +494,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.reply == null) {
|
if (data.reply == null) {
|
||||||
|
// TODO: キャッシュ
|
||||||
this.followingsRepository.findBy({
|
this.followingsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
notify: 'normal',
|
notify: 'normal',
|
||||||
}).then(followings => {
|
}).then(followings => {
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
|
// TODO: ワードミュート考慮
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, user.id);
|
}, user.id);
|
||||||
@ -811,6 +799,205 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
return mentionedUsers;
|
return mentionedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||||
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
|
if (note.channelId) {
|
||||||
|
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followeeId: note.channelId,
|
||||||
|
},
|
||||||
|
select: ['followerId'],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const channelFollowing of channelFollowings) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${channelFollowing.followerId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${channelFollowing.followerId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: キャッシュ?
|
||||||
|
const followings = await this.followingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followeeId: user.id,
|
||||||
|
followerHost: IsNull(),
|
||||||
|
isFollowerHibernated: false,
|
||||||
|
},
|
||||||
|
select: ['followerId', 'withReplies'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const userListMemberships = await this.userListMembershipsRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
select: ['userListId', 'withReplies'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
|
||||||
|
for (const following of followings) {
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
if (!following.withReplies) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${following.followerId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${following.followerId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
//if (note.visibility === 'followers') {
|
||||||
|
// // TODO: 重そうだから何とかしたい Set 使う?
|
||||||
|
// userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
|
||||||
|
//}
|
||||||
|
|
||||||
|
for (const userListMembership of userListMemberships) {
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
if (!userListMembership.withReplies) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userListTimeline:${userListMembership.userListId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserListTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userListTimelineWithFiles:${userListMembership.userListId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserListTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // 自分自身のHTL
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${user.id}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${user.id}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimelineWithReplies:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
} else {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimeline:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimelineWithFiles:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? (meta.perLocalUserUserTimelineCacheMax / 2).toString() : (meta.perRemoteUserUserTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
'localTimeline',
|
||||||
|
'MAXLEN', '~', '1000',
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
'localTimelineWithFiles',
|
||||||
|
'MAXLEN', '~', '500',
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.checkHibernation(followings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async checkHibernation(followings: MiFollowing[]) {
|
||||||
|
if (followings.length === 0) return;
|
||||||
|
|
||||||
|
const shuffle = (array: MiFollowing[]) => {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ランダムに最大1000件サンプリング
|
||||||
|
const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
|
||||||
|
|
||||||
|
const hibernatedUsers = await this.usersRepository.find({
|
||||||
|
where: {
|
||||||
|
id: In(samples.map(x => x.followerId)),
|
||||||
|
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
|
||||||
|
},
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hibernatedUsers.length > 0) {
|
||||||
|
this.usersRepository.update({
|
||||||
|
id: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isHibernated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
|
@ -99,19 +99,19 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (recieveConfig?.type === 'following') {
|
if (recieveConfig?.type === 'following') {
|
||||||
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
|
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId));
|
||||||
if (!isFollowing) {
|
if (!isFollowing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (recieveConfig?.type === 'follower') {
|
} else if (recieveConfig?.type === 'follower') {
|
||||||
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
|
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId));
|
||||||
if (!isFollower) {
|
if (!isFollower) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (recieveConfig?.type === 'mutualFollow') {
|
} else if (recieveConfig?.type === 'mutualFollow') {
|
||||||
const [isFollowing, isFollower] = await Promise.all([
|
const [isFollowing, isFollower] = await Promise.all([
|
||||||
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
|
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||||
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
|
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||||
]);
|
]);
|
||||||
if (!isFollowing && !isFollower) {
|
if (!isFollowing && !isFollower) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { SelectQueryBuilder } from 'typeorm';
|
import type { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
@ -23,9 +23,6 @@ export class QueryService {
|
|||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
@ -108,39 +105,6 @@ export class QueryService {
|
|||||||
q.setParameters(blockedQuery.getParameters());
|
q.setParameters(blockedQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
|
||||||
if (me == null) {
|
|
||||||
q.andWhere('note.channelId IS NULL');
|
|
||||||
} else {
|
|
||||||
q.leftJoinAndSelect('note.channel', 'channel');
|
|
||||||
|
|
||||||
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
|
|
||||||
.select('channelFollowing.followeeId')
|
|
||||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
// チャンネルのノートではない
|
|
||||||
.where('note.channelId IS NULL')
|
|
||||||
// または自分がフォローしているチャンネルのノート
|
|
||||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
|
||||||
}));
|
|
||||||
|
|
||||||
q.setParameters(channelFollowingQuery.getParameters());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
|
||||||
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
|
|
||||||
.select('muted.noteId')
|
|
||||||
.where('muted.userId = :userId', { userId: me.id });
|
|
||||||
|
|
||||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
|
||||||
|
|
||||||
q.setParameters(mutedQuery.getParameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||||
@ -212,32 +176,6 @@ export class QueryService {
|
|||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void {
|
|
||||||
if (me == null) {
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.replyUserId = note.userId');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
} else if (!withReplies) {
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
|
||||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.userId = :meId', { meId: me.id });
|
|
||||||
}))
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.replyUserId = note.userId');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
||||||
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
||||||
|
@ -26,7 +26,6 @@ export type RolePolicies = {
|
|||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
canEditNote: boolean;
|
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
inviteLimit: number;
|
inviteLimit: number;
|
||||||
inviteLimitCycle: number;
|
inviteLimitCycle: number;
|
||||||
@ -52,7 +51,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
canEditNote: true,
|
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
inviteLimit: 0,
|
inviteLimit: 0,
|
||||||
inviteLimitCycle: 60 * 24 * 7,
|
inviteLimitCycle: 60 * 24 * 7,
|
||||||
@ -298,7 +296,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||||
canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
|
|
||||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||||
|
@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/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 { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.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';
|
||||||
@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const userList of userLists) {
|
for (const userList of userLists) {
|
||||||
await this.userListJoiningsRepository.delete({
|
await this.userListMembershipsRepository.delete({
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
@ -123,7 +123,11 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
||||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||||
if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) {
|
if (
|
||||||
|
followee.isLocked ||
|
||||||
|
(followeeProfile.carefulBot && follower.isBot) ||
|
||||||
|
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
|
||||||
|
) {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { UserListJoiningsRepository } from '@/models/_.js';
|
import type { UserListMembershipsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import type { MiUserListJoining } from '@/models/UserListJoining.js';
|
import type { MiUserListMembership } from '@/models/UserListMembership.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 { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown {
|
|||||||
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
memoryCacheLifetime: 1000 * 60, // 1m
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
||||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
});
|
});
|
||||||
@ -85,19 +85,19 @@ export class UserListService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
||||||
const currentCount = await this.userListJoiningsRepository.countBy({
|
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||||
throw new UserListService.TooManyUsersError();
|
throw new UserListService.TooManyUsersError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userListJoiningsRepository.insert({
|
await this.userListMembershipsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as MiUserListJoining);
|
} as MiUserListMembership);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||||
@ -113,7 +113,7 @@ export class UserListService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async removeMember(target: MiUser, list: MiUserList) {
|
public async removeMember(target: MiUser, list: MiUserList) {
|
||||||
await this.userListJoiningsRepository.delete({
|
await this.userListMembershipsRepository.delete({
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
@ -122,6 +122,24 @@ export class UserListService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
|
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) {
|
||||||
|
const membership = await this.userListMembershipsRepository.findOneBy({
|
||||||
|
userId: target.id,
|
||||||
|
userListId: list.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (membership == null) {
|
||||||
|
throw new Error('User is not a member of the list');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userListMembershipsRepository.update({
|
||||||
|
id: membership.id,
|
||||||
|
}, {
|
||||||
|
withReplies: options.withReplies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
53
packages/backend/src/core/UserService.ts
Normal file
53
packages/backend/src/core/UserService.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateLastActiveDate(user: MiUser): Promise<void> {
|
||||||
|
if (user.isHibernated) {
|
||||||
|
const result = await this.usersRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: user.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => {
|
||||||
|
return response.raw[0];
|
||||||
|
});
|
||||||
|
const wokeUp = result.isHibernated;
|
||||||
|
if (wokeUp) {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
isHibernated: false,
|
||||||
|
});
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: user.id,
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -98,13 +98,13 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
} else if (meId === packedNote.userId) {
|
} else if (meId === packedNote.userId) {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||||
// 自分の投稿に対するリプライ
|
// 自分の投稿に対するリプライ
|
||||||
hide = false;
|
hide = false;
|
||||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||||
// 自分へのメンション
|
// 自分へのメンション
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
followeeId: packedNote.userId,
|
followeeId: packedNote.userId,
|
||||||
@ -308,7 +308,6 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
const packed: Packed<'Note'> = await awaitAll({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: note.createdAt.toISOString(),
|
createdAt: note.createdAt.toISOString(),
|
||||||
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
|
@ -487,6 +487,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
isMuted: relation.isMuted,
|
isMuted: relation.isMuted,
|
||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
notify: relation.following?.notify ?? 'none',
|
notify: relation.following?.notify ?? 'none',
|
||||||
|
withReplies: relation.following?.withReplies ?? false,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||||
|
|
||||||
|
@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
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 { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListEntityService {
|
export class UserListEntityService {
|
||||||
@ -17,8 +18,10 @@ export class UserListEntityService {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +31,7 @@ export class UserListEntityService {
|
|||||||
): Promise<Packed<'UserList'>> {
|
): Promise<Packed<'UserList'>> {
|
||||||
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
|
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const users = await this.userListJoiningsRepository.findBy({
|
const users = await this.userListMembershipsRepository.findBy({
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,5 +43,18 @@ export class UserListEntityService {
|
|||||||
isPublic: userList.isPublic,
|
isPublic: userList.isPublic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packMembershipsMany(
|
||||||
|
memberships: MiUserListMembership[],
|
||||||
|
) {
|
||||||
|
return Promise.all(memberships.map(async x => ({
|
||||||
|
id: x.id,
|
||||||
|
createdAt: x.createdAt.toISOString(),
|
||||||
|
userId: x.userId,
|
||||||
|
user: await this.userEntityService.pack(x.userId),
|
||||||
|
withReplies: x.withReplies,
|
||||||
|
})));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export const DI = {
|
|||||||
redis: Symbol('redis'),
|
redis: Symbol('redis'),
|
||||||
redisForPub: Symbol('redisForPub'),
|
redisForPub: Symbol('redisForPub'),
|
||||||
redisForSub: Symbol('redisForSub'),
|
redisForSub: Symbol('redisForSub'),
|
||||||
|
redisForTimelines: Symbol('redisForTimelines'),
|
||||||
|
|
||||||
//#region Repositories
|
//#region Repositories
|
||||||
usersRepository: Symbol('usersRepository'),
|
usersRepository: Symbol('usersRepository'),
|
||||||
@ -30,7 +31,7 @@ export const DI = {
|
|||||||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||||
userListsRepository: Symbol('userListsRepository'),
|
userListsRepository: Symbol('userListsRepository'),
|
||||||
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||||
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
userListMembershipsRepository: Symbol('userListMembershipsRepository'),
|
||||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||||
userIpsRepository: Symbol('userIpsRepository'),
|
userIpsRepository: Symbol('userIpsRepository'),
|
||||||
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
||||||
@ -63,7 +64,6 @@ export const DI = {
|
|||||||
promoNotesRepository: Symbol('promoNotesRepository'),
|
promoNotesRepository: Symbol('promoNotesRepository'),
|
||||||
promoReadsRepository: Symbol('promoReadsRepository'),
|
promoReadsRepository: Symbol('promoReadsRepository'),
|
||||||
relaysRepository: Symbol('relaysRepository'),
|
relaysRepository: Symbol('relaysRepository'),
|
||||||
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
|
||||||
channelsRepository: Symbol('channelsRepository'),
|
channelsRepository: Symbol('channelsRepository'),
|
||||||
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
||||||
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
||||||
|
@ -9,6 +9,7 @@ import { MiUser } from './User.js';
|
|||||||
|
|
||||||
@Entity('following')
|
@Entity('following')
|
||||||
@Index(['followerId', 'followeeId'], { unique: true })
|
@Index(['followerId', 'followeeId'], { unique: true })
|
||||||
|
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||||
export class MiFollowing {
|
export class MiFollowing {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
@ -45,6 +46,17 @@ export class MiFollowing {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public follower: MiUser | null;
|
public follower: MiUser | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isFollowerHibernated: boolean;
|
||||||
|
|
||||||
|
// タイムラインにその人のリプライまで含めるかどうか
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public withReplies: boolean;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32,
|
length: 32,
|
||||||
|
@ -471,4 +471,24 @@ export class MiMeta {
|
|||||||
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
||||||
})
|
})
|
||||||
public preservedUsernames: string[];
|
public preservedUsernames: string[];
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perLocalUserUserTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 100,
|
||||||
|
})
|
||||||
|
public perRemoteUserUserTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perUserHomeTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perUserListTimelineCacheMax: number;
|
||||||
}
|
}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
|
|
||||||
import { mutedNoteReasons } from '@/types.js';
|
|
||||||
import { id } from './util/id.js';
|
|
||||||
import { MiNote } from './Note.js';
|
|
||||||
import { MiUser } from './User.js';
|
|
||||||
|
|
||||||
@Entity('muted_note')
|
|
||||||
@Index(['noteId', 'userId'], { unique: true })
|
|
||||||
export class MiMutedNote {
|
|
||||||
@PrimaryColumn(id())
|
|
||||||
public id: string;
|
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
comment: 'The note ID.',
|
|
||||||
})
|
|
||||||
public noteId: MiNote['id'];
|
|
||||||
|
|
||||||
@ManyToOne(type => MiNote, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public note: MiNote | null;
|
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
comment: 'The user ID.',
|
|
||||||
})
|
|
||||||
public userId: MiUser['id'];
|
|
||||||
|
|
||||||
@ManyToOne(type => MiUser, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public user: MiUser | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ミュートされた理由。
|
|
||||||
*/
|
|
||||||
@Index()
|
|
||||||
@Column('enum', {
|
|
||||||
enum: mutedNoteReasons,
|
|
||||||
comment: 'The reason of the MutedNote.',
|
|
||||||
})
|
|
||||||
public reason: typeof mutedNoteReasons[number];
|
|
||||||
}
|
|
@ -24,11 +24,6 @@ export class MiNote {
|
|||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
@Column('timestamp with time zone', {
|
|
||||||
default: null,
|
|
||||||
})
|
|
||||||
public updatedAt: Date | null;
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
|
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $userListJoiningsRepository: Provider = {
|
const $userListMembershipsRepository: Provider = {
|
||||||
provide: DI.userListJoiningsRepository,
|
provide: DI.userListMembershipsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiUserListJoining),
|
useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -315,12 +315,6 @@ const $relaysRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $mutedNotesRepository: Provider = {
|
|
||||||
provide: DI.mutedNotesRepository,
|
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiMutedNote),
|
|
||||||
inject: [DI.db],
|
|
||||||
};
|
|
||||||
|
|
||||||
const $channelsRepository: Provider = {
|
const $channelsRepository: Provider = {
|
||||||
provide: DI.channelsRepository,
|
provide: DI.channelsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiChannel),
|
useFactory: (db: DataSource) => db.getRepository(MiChannel),
|
||||||
@ -421,7 +415,7 @@ const $userMemosRepository: Provider = {
|
|||||||
$userPublickeysRepository,
|
$userPublickeysRepository,
|
||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
$userListFavoritesRepository,
|
$userListFavoritesRepository,
|
||||||
$userListJoiningsRepository,
|
$userListMembershipsRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
$usedUsernamesRepository,
|
$usedUsernamesRepository,
|
||||||
@ -454,7 +448,6 @@ const $userMemosRepository: Provider = {
|
|||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
$mutedNotesRepository,
|
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
$channelFavoritesRepository,
|
$channelFavoritesRepository,
|
||||||
@ -488,7 +481,7 @@ const $userMemosRepository: Provider = {
|
|||||||
$userPublickeysRepository,
|
$userPublickeysRepository,
|
||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
$userListFavoritesRepository,
|
$userListFavoritesRepository,
|
||||||
$userListJoiningsRepository,
|
$userListMembershipsRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
$usedUsernamesRepository,
|
$usedUsernamesRepository,
|
||||||
@ -521,7 +514,6 @@ const $userMemosRepository: Provider = {
|
|||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
$mutedNotesRepository,
|
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
$channelFavoritesRepository,
|
$channelFavoritesRepository,
|
||||||
|
@ -187,6 +187,11 @@ export class MiUser {
|
|||||||
})
|
})
|
||||||
public isExplorable: boolean;
|
public isExplorable: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isHibernated: boolean;
|
||||||
|
|
||||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -8,14 +8,14 @@ import { id } from './util/id.js';
|
|||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiUserList } from './UserList.js';
|
import { MiUserList } from './UserList.js';
|
||||||
|
|
||||||
@Entity('user_list_joining')
|
@Entity('user_list_membership')
|
||||||
@Index(['userId', 'userListId'], { unique: true })
|
@Index(['userId', 'userListId'], { unique: true })
|
||||||
export class MiUserListJoining {
|
export class MiUserListMembership {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
comment: 'The created date of the UserListJoining.',
|
comment: 'The created date of the UserListMembership.',
|
||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
@ -44,4 +44,10 @@ export class MiUserListJoining {
|
|||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public userList: MiUserList | null;
|
public userList: MiUserList | null;
|
||||||
|
|
||||||
|
// タイムラインにその人のリプライまで含めるかどうか
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public withReplies: boolean;
|
||||||
}
|
}
|
@ -28,7 +28,6 @@ import { MiHashtag } from '@/models/Hashtag.js';
|
|||||||
import { MiInstance } from '@/models/Instance.js';
|
import { MiInstance } from '@/models/Instance.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMutedNote } from '@/models/MutedNote.js';
|
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
@ -53,7 +52,7 @@ import { MiUser } from '@/models/User.js';
|
|||||||
import { MiUserIp } from '@/models/UserIp.js';
|
import { MiUserIp } from '@/models/UserIp.js';
|
||||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import { MiUserList } from '@/models/UserList.js';
|
import { MiUserList } from '@/models/UserList.js';
|
||||||
import { MiUserListJoining } from '@/models/UserListJoining.js';
|
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||||
import { MiUserPending } from '@/models/UserPending.js';
|
import { MiUserPending } from '@/models/UserPending.js';
|
||||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
@ -96,7 +95,6 @@ export {
|
|||||||
MiInstance,
|
MiInstance,
|
||||||
MiMeta,
|
MiMeta,
|
||||||
MiModerationLog,
|
MiModerationLog,
|
||||||
MiMutedNote,
|
|
||||||
MiMuting,
|
MiMuting,
|
||||||
MiRenoteMuting,
|
MiRenoteMuting,
|
||||||
MiNote,
|
MiNote,
|
||||||
@ -122,7 +120,7 @@ export {
|
|||||||
MiUserKeypair,
|
MiUserKeypair,
|
||||||
MiUserList,
|
MiUserList,
|
||||||
MiUserListFavorite,
|
MiUserListFavorite,
|
||||||
MiUserListJoining,
|
MiUserListMembership,
|
||||||
MiUserNotePining,
|
MiUserNotePining,
|
||||||
MiUserPending,
|
MiUserPending,
|
||||||
MiUserProfile,
|
MiUserProfile,
|
||||||
@ -163,7 +161,6 @@ export type HashtagsRepository = Repository<MiHashtag>;
|
|||||||
export type InstancesRepository = Repository<MiInstance>;
|
export type InstancesRepository = Repository<MiInstance>;
|
||||||
export type MetasRepository = Repository<MiMeta>;
|
export type MetasRepository = Repository<MiMeta>;
|
||||||
export type ModerationLogsRepository = Repository<MiModerationLog>;
|
export type ModerationLogsRepository = Repository<MiModerationLog>;
|
||||||
export type MutedNotesRepository = Repository<MiMutedNote>;
|
|
||||||
export type MutingsRepository = Repository<MiMuting>;
|
export type MutingsRepository = Repository<MiMuting>;
|
||||||
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
||||||
export type NotesRepository = Repository<MiNote>;
|
export type NotesRepository = Repository<MiNote>;
|
||||||
@ -189,7 +186,7 @@ export type UserIpsRepository = Repository<MiUserIp>;
|
|||||||
export type UserKeypairsRepository = Repository<MiUserKeypair>;
|
export type UserKeypairsRepository = Repository<MiUserKeypair>;
|
||||||
export type UserListsRepository = Repository<MiUserList>;
|
export type UserListsRepository = Repository<MiUserList>;
|
||||||
export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
|
export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
|
||||||
export type UserListJoiningsRepository = Repository<MiUserListJoining>;
|
export type UserListMembershipsRepository = Repository<MiUserListMembership>;
|
||||||
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
|
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
|
||||||
export type UserPendingsRepository = Repository<MiUserPending>;
|
export type UserPendingsRepository = Repository<MiUserPending>;
|
||||||
export type UserProfilesRepository = Repository<MiUserProfile>;
|
export type UserProfilesRepository = Repository<MiUserProfile>;
|
||||||
|
@ -17,11 +17,6 @@ export const packedNoteSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
updatedAt: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
format: 'date-time',
|
|
||||||
},
|
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -277,6 +277,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
withReplies: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -36,7 +36,6 @@ import { MiHashtag } from '@/models/Hashtag.js';
|
|||||||
import { MiInstance } from '@/models/Instance.js';
|
import { MiInstance } from '@/models/Instance.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMutedNote } from '@/models/MutedNote.js';
|
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
@ -62,7 +61,7 @@ import { MiUserIp } from '@/models/UserIp.js';
|
|||||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import { MiUserList } from '@/models/UserList.js';
|
import { MiUserList } from '@/models/UserList.js';
|
||||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||||
import { MiUserListJoining } from '@/models/UserListJoining.js';
|
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||||
import { MiUserPending } from '@/models/UserPending.js';
|
import { MiUserPending } from '@/models/UserPending.js';
|
||||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
@ -138,7 +137,7 @@ export const entities = [
|
|||||||
MiUserPublickey,
|
MiUserPublickey,
|
||||||
MiUserList,
|
MiUserList,
|
||||||
MiUserListFavorite,
|
MiUserListFavorite,
|
||||||
MiUserListJoining,
|
MiUserListMembership,
|
||||||
MiUserNotePining,
|
MiUserNotePining,
|
||||||
MiUserSecurityKey,
|
MiUserSecurityKey,
|
||||||
MiUsedUsername,
|
MiUsedUsername,
|
||||||
@ -174,7 +173,6 @@ export const entities = [
|
|||||||
MiPromoNote,
|
MiPromoNote,
|
||||||
MiPromoRead,
|
MiPromoRead,
|
||||||
MiRelay,
|
MiRelay,
|
||||||
MiMutedNote,
|
|
||||||
MiChannel,
|
MiChannel,
|
||||||
MiChannelFollowing,
|
MiChannelFollowing,
|
||||||
MiChannelFavorite,
|
MiChannelFavorite,
|
||||||
|
@ -6,7 +6,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/_.js';
|
import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
@ -25,9 +25,6 @@ export class CleanProcessorService {
|
|||||||
@Inject(DI.userIpsRepository)
|
@Inject(DI.userIpsRepository)
|
||||||
private userIpsRepository: UserIpsRepository,
|
private userIpsRepository: UserIpsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@ -48,16 +45,6 @@ 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.mutedNotesRepository.delete({
|
|
||||||
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
|
||||||
reason: 'word',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mutedNotesRepository.delete({
|
|
||||||
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
|
||||||
reason: 'word',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使われてないアンテナを停止
|
// 使われてないアンテナを停止
|
||||||
if (this.config.deactivateAntennaThreshold > 0) {
|
if (this.config.deactivateAntennaThreshold > 0) {
|
||||||
this.antennasRepository.update({
|
this.antennasRepository.update({
|
||||||
|
@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { format as DateFormat } from 'date-fns';
|
import { format as DateFormat } from 'date-fns';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js';
|
import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@ -29,8 +29,8 @@ export class ExportAntennasProcessorService {
|
|||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennsRepository: AntennasRepository,
|
private antennsRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
@ -65,9 +65,9 @@ export class ExportAntennasProcessorService {
|
|||||||
for (const [index, antenna] of antennas.entries()) {
|
for (const [index, antenna] of antennas.entries()) {
|
||||||
let users: MiUser[] | undefined;
|
let users: MiUser[] | undefined;
|
||||||
if (antenna.userListId !== null) {
|
if (antenna.userListId !== null) {
|
||||||
const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId });
|
const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId });
|
||||||
users = await this.usersRepository.findBy({
|
users = await this.usersRepository.findBy({
|
||||||
id: In(joinings.map(j => j.userId)),
|
id: In(memberships.map(j => j.userId)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
write(JSON.stringify({
|
write(JSON.stringify({
|
||||||
|
@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
|
import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
@ -29,8 +29,8 @@ export class ExportUserListsProcessorService {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
@ -61,9 +61,9 @@ export class ExportUserListsProcessorService {
|
|||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
|
||||||
for (const list of lists) {
|
for (const list of lists) {
|
||||||
const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id });
|
const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id });
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(joinings.map(j => j.userId)),
|
id: In(memberships.map(j => j.userId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
@ -33,8 +33,8 @@ export class ImportUserListsProcessorService {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@ -99,7 +99,7 @@ export class ImportUserListsProcessorService {
|
|||||||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||||
|
|
||||||
this.userListService.addMember(target, list!, user);
|
this.userListService.addMember(target, list!, user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
|||||||
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
||||||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||||
import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js';
|
|
||||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||||
@ -258,7 +257,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
|||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||||
import * as ep___notes_update from './endpoints/notes/update.js';
|
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
@ -336,7 +334,9 @@ import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
|||||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||||
|
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||||
|
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||||
@ -554,7 +554,6 @@ const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass:
|
|||||||
const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
|
const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
|
||||||
const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
|
const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
|
||||||
const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
|
const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
|
||||||
const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default };
|
|
||||||
const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default };
|
const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default };
|
||||||
const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
|
const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
|
||||||
const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
|
const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
|
||||||
@ -607,7 +606,6 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
|
|||||||
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
|
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
|
||||||
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
|
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
|
||||||
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
|
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
|
||||||
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
|
|
||||||
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
|
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
|
||||||
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
||||||
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
||||||
@ -685,7 +683,9 @@ const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass:
|
|||||||
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
||||||
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
||||||
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
||||||
const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
|
const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default };
|
||||||
|
const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default };
|
||||||
|
const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default };
|
||||||
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
||||||
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
||||||
const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
|
const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
|
||||||
@ -907,7 +907,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$i_favorites,
|
$i_favorites,
|
||||||
$i_gallery_likes,
|
$i_gallery_likes,
|
||||||
$i_gallery_posts,
|
$i_gallery_posts,
|
||||||
$i_getWordMutedNotesCount,
|
|
||||||
$i_importBlocking,
|
$i_importBlocking,
|
||||||
$i_importFollowing,
|
$i_importFollowing,
|
||||||
$i_importMuting,
|
$i_importMuting,
|
||||||
@ -960,7 +959,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
$notes_update,
|
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
@ -1038,7 +1036,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$users_lists_update,
|
$users_lists_update,
|
||||||
$users_lists_favorite,
|
$users_lists_favorite,
|
||||||
$users_lists_unfavorite,
|
$users_lists_unfavorite,
|
||||||
$users_lists_create_from_public,
|
$users_lists_createFromPublic,
|
||||||
|
$users_lists_updateMembership,
|
||||||
|
$users_lists_getMemberships,
|
||||||
$users_notes,
|
$users_notes,
|
||||||
$users_pages,
|
$users_pages,
|
||||||
$users_flashs,
|
$users_flashs,
|
||||||
@ -1254,7 +1254,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$i_favorites,
|
$i_favorites,
|
||||||
$i_gallery_likes,
|
$i_gallery_likes,
|
||||||
$i_gallery_posts,
|
$i_gallery_posts,
|
||||||
$i_getWordMutedNotesCount,
|
|
||||||
$i_importBlocking,
|
$i_importBlocking,
|
||||||
$i_importFollowing,
|
$i_importFollowing,
|
||||||
$i_importMuting,
|
$i_importMuting,
|
||||||
@ -1307,7 +1306,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
$notes_update,
|
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
@ -1382,7 +1380,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$users_lists_update,
|
$users_lists_update,
|
||||||
$users_lists_favorite,
|
$users_lists_favorite,
|
||||||
$users_lists_unfavorite,
|
$users_lists_unfavorite,
|
||||||
$users_lists_create_from_public,
|
$users_lists_createFromPublic,
|
||||||
|
$users_lists_updateMembership,
|
||||||
|
$users_lists_getMemberships,
|
||||||
$users_notes,
|
$users_notes,
|
||||||
$users_pages,
|
$users_pages,
|
||||||
$users_flashs,
|
$users_flashs,
|
||||||
|
@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
|
import { UserService } from '@/core/UserService.js';
|
||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/Connection.js';
|
import MainStreamConnection from './stream/Connection.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
import { ChannelsService } from './stream/ChannelsService.js';
|
||||||
@ -37,6 +38,7 @@ export class StreamingApiServerService {
|
|||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private channelsService: ChannelsService,
|
private channelsService: ChannelsService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
|
private usersService: UserService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,14 +132,10 @@ export class StreamingApiServerService {
|
|||||||
this.#connections.set(connection, Date.now());
|
this.#connections.set(connection, Date.now());
|
||||||
|
|
||||||
const userUpdateIntervalId = user ? setInterval(() => {
|
const userUpdateIntervalId = user ? setInterval(() => {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersService.updateLastActiveDate(user);
|
||||||
lastActiveDate: new Date(),
|
|
||||||
});
|
|
||||||
}, 1000 * 60 * 5) : null;
|
}, 1000 * 60 * 5) : null;
|
||||||
if (user) {
|
if (user) {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersService.updateLastActiveDate(user);
|
||||||
lastActiveDate: new Date(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.once('close', () => {
|
connection.once('close', () => {
|
||||||
|
@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
|||||||
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
||||||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||||
import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js';
|
|
||||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||||
@ -258,7 +257,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
|||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||||
import * as ep___notes_update from './endpoints/notes/update.js';
|
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
@ -335,8 +333,10 @@ import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
|||||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||||
|
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||||
|
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||||
@ -552,7 +552,6 @@ const eps = [
|
|||||||
['i/favorites', ep___i_favorites],
|
['i/favorites', ep___i_favorites],
|
||||||
['i/gallery/likes', ep___i_gallery_likes],
|
['i/gallery/likes', ep___i_gallery_likes],
|
||||||
['i/gallery/posts', ep___i_gallery_posts],
|
['i/gallery/posts', ep___i_gallery_posts],
|
||||||
['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount],
|
|
||||||
['i/import-blocking', ep___i_importBlocking],
|
['i/import-blocking', ep___i_importBlocking],
|
||||||
['i/import-following', ep___i_importFollowing],
|
['i/import-following', ep___i_importFollowing],
|
||||||
['i/import-muting', ep___i_importMuting],
|
['i/import-muting', ep___i_importMuting],
|
||||||
@ -605,7 +604,6 @@ const eps = [
|
|||||||
['notes/conversation', ep___notes_conversation],
|
['notes/conversation', ep___notes_conversation],
|
||||||
['notes/create', ep___notes_create],
|
['notes/create', ep___notes_create],
|
||||||
['notes/delete', ep___notes_delete],
|
['notes/delete', ep___notes_delete],
|
||||||
['notes/update', ep___notes_update],
|
|
||||||
['notes/favorites/create', ep___notes_favorites_create],
|
['notes/favorites/create', ep___notes_favorites_create],
|
||||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||||
['notes/featured', ep___notes_featured],
|
['notes/featured', ep___notes_featured],
|
||||||
@ -683,7 +681,9 @@ const eps = [
|
|||||||
['users/lists/favorite', ep___users_lists_favorite],
|
['users/lists/favorite', ep___users_lists_favorite],
|
||||||
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
||||||
['users/lists/update', ep___users_lists_update],
|
['users/lists/update', ep___users_lists_update],
|
||||||
['users/lists/create-from-public', ep___users_lists_create_from_public],
|
['users/lists/create-from-public', ep___users_lists_createFromPublic],
|
||||||
|
['users/lists/update-membership', ep___users_lists_updateMembership],
|
||||||
|
['users/lists/get-memberships', ep___users_lists_getMemberships],
|
||||||
['users/notes', ep___users_notes],
|
['users/notes', ep___users_notes],
|
||||||
['users/pages', ep___users_pages],
|
['users/pages', ep___users_pages],
|
||||||
['users/flashs', ep___users_flashs],
|
['users/flashs', ep___users_flashs],
|
||||||
|
@ -105,40 +105,32 @@ export const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
userStarForReactionFallback: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
pinnedUsers: {
|
pinnedUsers: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hiddenTags: {
|
hiddenTags: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
blockedHosts: {
|
blockedHosts: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sensitiveWords: {
|
sensitiveWords: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
preservedUsernames: {
|
preservedUsernames: {
|
||||||
@ -146,129 +138,124 @@ export const meta = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hcaptchaSecretKey: {
|
hcaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
recaptchaSecretKey: {
|
recaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
turnstileSecretKey: {
|
turnstileSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
sensitiveMediaDetection: {
|
sensitiveMediaDetection: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
sensitiveMediaDetectionSensitivity: {
|
sensitiveMediaDetectionSensitivity: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
setSensitiveFlagAutomatically: {
|
setSensitiveFlagAutomatically: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableSensitiveMediaDetectionForVideos: {
|
enableSensitiveMediaDetectionForVideos: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
proxyAccountId: {
|
proxyAccountId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
summaryProxy: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
email: {
|
email: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
smtpSecure: {
|
smtpSecure: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
smtpHost: {
|
smtpHost: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
smtpPort: {
|
smtpPort: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
smtpUser: {
|
smtpUser: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
smtpPass: {
|
smtpPass: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
swPrivateKey: {
|
swPrivateKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
useObjectStorage: {
|
useObjectStorage: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
objectStorageBaseUrl: {
|
objectStorageBaseUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageBucket: {
|
objectStorageBucket: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStoragePrefix: {
|
objectStoragePrefix: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageEndpoint: {
|
objectStorageEndpoint: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageRegion: {
|
objectStorageRegion: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStoragePort: {
|
objectStoragePort: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageAccessKey: {
|
objectStorageAccessKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageSecretKey: {
|
objectStorageSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
objectStorageUseSSL: {
|
objectStorageUseSSL: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
objectStorageUseProxy: {
|
objectStorageUseProxy: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
objectStorageSetPublicRead: {
|
objectStorageSetPublicRead: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableIpLogging: {
|
enableIpLogging: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableActiveEmailValidation: {
|
enableActiveEmailValidation: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableChartsForRemoteUser: {
|
enableChartsForRemoteUser: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -288,12 +275,28 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
manifestJsonOverride: {
|
manifestJsonOverride: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
policies: {
|
policies: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
perLocalUserUserTimelineCacheMax: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
perRemoteUserUserTimelineCacheMax: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
perUserHomeTimelineCacheMax: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
perUserListTimelineCacheMax: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@ -313,7 +316,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async () => {
|
||||||
const instance = await this.metaService.fetch(true);
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -399,6 +402,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||||
manifestJsonOverride: instance.manifestJsonOverride,
|
manifestJsonOverride: instance.manifestJsonOverride,
|
||||||
|
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
|
||||||
|
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
|
||||||
|
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||||
|
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,10 @@ export const paramDef = {
|
|||||||
serverRules: { type: 'array', items: { type: 'string' } },
|
serverRules: { type: 'array', items: { type: 'string' } },
|
||||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||||
manifestJsonOverride: { type: 'string' },
|
manifestJsonOverride: { type: 'string' },
|
||||||
|
perLocalUserUserTimelineCacheMax: { type: 'integer' },
|
||||||
|
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
|
||||||
|
perUserHomeTimelineCacheMax: { type: 'integer' },
|
||||||
|
perUserListTimelineCacheMax: { type: 'integer' },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
@ -441,6 +445,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.manifestJsonOverride = ps.manifestJsonOverride;
|
set.manifestJsonOverride = ps.manifestJsonOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
|
||||||
|
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.perRemoteUserUserTimelineCacheMax !== undefined) {
|
||||||
|
set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.perUserHomeTimelineCacheMax !== undefined) {
|
||||||
|
set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.perUserListTimelineCacheMax !== undefined) {
|
||||||
|
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
@ -56,8 +56,8 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
@ -86,7 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
});
|
});
|
||||||
|
|
||||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const noteIdsRes = await this.redisClient.xrevrange(
|
const noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
`antennaTimeline:${antenna.id}`,
|
`antennaTimeline:${antenna.id}`,
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
|
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
|
||||||
|
@ -54,8 +54,8 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
let noteIdsRes: [string, string[]][] = [];
|
let noteIdsRes: [string, string[]][] = [];
|
||||||
|
|
||||||
if (!ps.sinceId && !ps.sinceDate) {
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
noteIdsRes = await this.redisClient.xrevrange(
|
noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
`channelTimeline:${channel.id}`,
|
`channelTimeline:${channel.id}`,
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
'-',
|
'-',
|
||||||
@ -104,7 +104,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -129,7 +128,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -57,8 +57,9 @@ export const paramDef = {
|
|||||||
properties: {
|
properties: {
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
notify: { type: 'string', enum: ['normal', 'none'] },
|
notify: { type: 'string', enum: ['normal', 'none'] },
|
||||||
|
withReplies: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['userId', 'notify'],
|
required: ['userId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -98,7 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
await this.followingsRepository.update({
|
await this.followingsRepository.update({
|
||||||
id: exist.id,
|
id: exist.id,
|
||||||
}, {
|
}, {
|
||||||
notify: ps.notify === 'none' ? null : ps.notify,
|
notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
|
||||||
|
withReplies: ps.withReplies != null ? ps.withReplies : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.userEntityService.pack(follower.id, me);
|
return await this.userEntityService.pack(follower.id, me);
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import type { MutedNotesRepository } from '@/models/_.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['account'],
|
|
||||||
|
|
||||||
requireCredential: true,
|
|
||||||
|
|
||||||
kind: 'read:account',
|
|
||||||
|
|
||||||
res: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
properties: {
|
|
||||||
count: {
|
|
||||||
type: 'number',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
|
||||||
) {
|
|
||||||
super(meta, paramDef, async (ps, me) => {
|
|
||||||
return {
|
|
||||||
count: await this.mutedNotesRepository.countBy({
|
|
||||||
userId: me.id,
|
|
||||||
reason: 'word',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -214,11 +214,11 @@ export const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
localTimeLine: {
|
localTimeline: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
globalTimeLine: {
|
globalTimeline: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
@ -40,7 +40,6 @@ export const paramDef = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withReplies: { type: 'boolean', default: false },
|
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
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' },
|
||||||
@ -68,49 +67,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.gtlDisabled);
|
throw new ApiError(meta.errors.gtlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Construct query
|
// TODO?
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
return [];
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
|
||||||
.andWhere('note.visibility = \'public\'')
|
|
||||||
.andWhere('note.channelId IS NULL')
|
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
|
||||||
|
|
||||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
if (me) {
|
|
||||||
this.activeUsersChart.read(me);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -51,7 +53,6 @@ export const paramDef = {
|
|||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withReplies: { type: 'boolean', default: false },
|
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
@ -60,17 +61,17 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const policies = await this.roleService.getUserPolicies(me.id);
|
const policies = await this.roleService.getUserPolicies(me.id);
|
||||||
@ -78,79 +79,77 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.stlDisabled);
|
throw new ApiError(meta.errors.stlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Construct query
|
const [
|
||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
userIdsWhoMeMuting,
|
||||||
.select('following.followeeId')
|
userIdsWhoMeMutingRenotes,
|
||||||
.where('following.followerId = :followerId', { followerId: me.id });
|
userIdsWhoBlockingMe,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]);
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
let timeline: MiNote[] = [];
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
|
||||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
.andWhere(new Brackets(qb => {
|
let htlNoteIdsRes: [string, string[]][] = [];
|
||||||
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
|
let ltlNoteIdsRes: [string, string[]][] = [];
|
||||||
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
|
||||||
}))
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
|
[htlNoteIdsRes, ltlNoteIdsRes] = await Promise.all([
|
||||||
|
this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit),
|
||||||
|
this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||||
|
const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||||
|
let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||||
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
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('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.setParameters(followingQuery.getParameters());
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateChannelQuery(query, me);
|
timeline = await query.getMany();
|
||||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
timeline = timeline.filter(note => {
|
||||||
query.andWhere(new Brackets(qb => {
|
if (note.userId === me.id) {
|
||||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
return true;
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
}
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
if (note.renoteId) {
|
||||||
}));
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
}
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.includeRenotedMyNotes === false) {
|
return true;
|
||||||
query.andWhere(new Brackets(qb => {
|
});
|
||||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.includeLocalRenotes === false) {
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.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 { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -41,11 +43,7 @@ export const paramDef = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withReplies: { type: 'boolean', default: false },
|
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
fileType: { type: 'array', items: {
|
|
||||||
type: 'string',
|
|
||||||
} },
|
|
||||||
excludeNsfw: { type: 'boolean', default: false },
|
excludeNsfw: { type: 'boolean', default: false },
|
||||||
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' },
|
||||||
@ -59,14 +57,17 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||||
@ -74,56 +75,63 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.ltlDisabled);
|
throw new ApiError(meta.errors.ltlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Construct query
|
const [
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
userIdsWhoMeMuting,
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
userIdsWhoMeMutingRenotes,
|
||||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
userIdsWhoBlockingMe,
|
||||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||||
|
|
||||||
|
let timeline: MiNote[] = [];
|
||||||
|
|
||||||
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
|
let noteIdsRes: [string, string[]][] = [];
|
||||||
|
|
||||||
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
|
noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateChannelQuery(query, me);
|
timeline = await query.getMany();
|
||||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
|
||||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
timeline = timeline.filter(note => {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
if (me && (note.userId === me.id)) {
|
||||||
}
|
return true;
|
||||||
|
}
|
||||||
if (ps.fileType != null) {
|
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
query.andWhere(new Brackets(qb => {
|
if (note.renoteId) {
|
||||||
for (const type of ps.fileType!) {
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
const i = ps.fileType!.indexOf(type);
|
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
if (ps.withRenotes === false) return false;
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
if (ps.excludeNsfw) {
|
|
||||||
query.andWhere('note.cw IS NULL');
|
|
||||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
return true;
|
||||||
query.andWhere(new Brackets(qb => {
|
});
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (me) {
|
if (me) {
|
||||||
|
@ -5,13 +5,16 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.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';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
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 { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@ -41,7 +44,6 @@ export const paramDef = {
|
|||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withReplies: { type: 'boolean', default: false },
|
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
@ -50,96 +52,82 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const followees = await this.followingsRepository.createQueryBuilder('following')
|
const [
|
||||||
.select('following.followeeId')
|
followings,
|
||||||
.where('following.followerId = :followerId', { followerId: me.id })
|
userIdsWhoMeMuting,
|
||||||
.getMany();
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]);
|
||||||
|
|
||||||
//#region Construct query
|
let timeline: MiNote[] = [];
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
// パフォーマンス上の利点が無さそう?
|
let noteIdsRes: [string, string[]][] = [];
|
||||||
//.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
|
||||||
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
|
noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
if (followees.length > 0) {
|
timeline = await query.getMany();
|
||||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
|
||||||
|
|
||||||
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
timeline = timeline.filter(note => {
|
||||||
} else {
|
if (note.userId === me.id) {
|
||||||
query.andWhere('note.userId = :meId', { meId: me.id });
|
return true;
|
||||||
}
|
}
|
||||||
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (note.reply && note.reply.visibility === 'followers') {
|
||||||
|
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.queryService.generateChannelQuery(query, me);
|
return true;
|
||||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
});
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.includeRenotedMyNotes === false) {
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.includeLocalRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ms from 'ms';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { UsersRepository, NotesRepository } from '@/models/_.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['notes'],
|
|
||||||
|
|
||||||
requireCredential: true,
|
|
||||||
requireRolePolicy: 'canEditNote',
|
|
||||||
|
|
||||||
kind: 'write:notes',
|
|
||||||
|
|
||||||
limit: {
|
|
||||||
duration: ms('1hour'),
|
|
||||||
max: 10,
|
|
||||||
minInterval: ms('1sec'),
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
noSuchNote: {
|
|
||||||
message: 'No such note.',
|
|
||||||
code: 'NO_SUCH_NOTE',
|
|
||||||
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
noteId: { type: 'string', format: 'misskey:id' },
|
|
||||||
text: {
|
|
||||||
type: 'string',
|
|
||||||
minLength: 1,
|
|
||||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
|
||||||
nullable: false,
|
|
||||||
},
|
|
||||||
cw: { type: 'string', nullable: true, maxLength: 100 },
|
|
||||||
},
|
|
||||||
required: ['noteId', 'text', 'cw'],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
private getterService: GetterService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
|
||||||
super(meta, paramDef, async (ps, me) => {
|
|
||||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
|
||||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (note.userId !== me.id) {
|
|
||||||
throw new ApiError(meta.errors.noSuchNote);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.notesRepository.update({ id: note.id }, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
cw: ps.cw,
|
|
||||||
text: ps.text,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
|
||||||
cw: ps.cw,
|
|
||||||
text: ps.text,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,12 +5,16 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.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';
|
||||||
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 { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@ -49,7 +53,6 @@ export const paramDef = {
|
|||||||
includeMyRenotes: { type: 'boolean', default: true },
|
includeMyRenotes: { type: 'boolean', default: true },
|
||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
withReplies: { type: 'boolean', default: false },
|
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
withFiles: {
|
withFiles: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -63,18 +66,19 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const list = await this.userListsRepository.findOneBy({
|
const list = await this.userListsRepository.findOneBy({
|
||||||
@ -86,72 +90,65 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.noSuchList);
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Construct query
|
const [
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
userIdsWhoMeMuting,
|
||||||
.innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let timeline: MiNote[] = [];
|
||||||
|
|
||||||
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
|
let noteIdsRes: [string, string[]][] = [];
|
||||||
|
|
||||||
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
|
noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
timeline = await query.getMany();
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
timeline = timeline.filter(note => {
|
||||||
query.andWhere(new Brackets(qb => {
|
if (note.userId === me.id) {
|
||||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
return true;
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
}
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
if (note.renoteId) {
|
||||||
}));
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
}
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.includeRenotedMyNotes === false) {
|
return true;
|
||||||
query.andWhere(new Brackets(qb => {
|
});
|
||||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.includeLocalRenotes === false) {
|
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ps.withReplies) {
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
query.andWhere('note.replyId IS NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
|
||||||
|
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const noteIdsRes = await this.redisClient.xrevrange(
|
const noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||||
`roleTimeline:${role.id}`,
|
`roleTimeline:${role.id}`,
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
|
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
name: ps.name,
|
name: ps.name,
|
||||||
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
|
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
const users = (await this.userListJoiningsRepository.findBy({
|
const users = (await this.userListMembershipsRepository.findBy({
|
||||||
userListId: ps.listId,
|
userListId: ps.listId,
|
||||||
})).map(x => x.userId);
|
})).map(x => x.userId);
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListJoiningsRepository.exist({
|
const exist = await this.userListMembershipsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['lists', 'account'],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
|
||||||
|
kind: 'read:account',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchList: {
|
||||||
|
message: 'No such list.',
|
||||||
|
code: 'NO_SUCH_LIST',
|
||||||
|
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
listId: { type: 'string', format: 'misskey:id' },
|
||||||
|
forPublic: { type: 'boolean', default: false },
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['listId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userListsRepository)
|
||||||
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userListMembershipsRepository)
|
||||||
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
|
private userListEntityService: UserListEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
// Fetch the list
|
||||||
|
const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
|
||||||
|
id: ps.listId,
|
||||||
|
userId: me.id,
|
||||||
|
} : {
|
||||||
|
id: ps.listId,
|
||||||
|
isPublic: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userList == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere('membership.userListId = :userListId', { userListId: userList.id })
|
||||||
|
.innerJoinAndSelect('membership.user', 'user');
|
||||||
|
|
||||||
|
const memberships = await query
|
||||||
|
.limit(ps.limit)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return this.userListEntityService.packMembershipsMany(memberships);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { UserListService } from '@/core/UserListService.js';
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListJoiningsRepository.exist({
|
const exist = await this.userListMembershipsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UserListsRepository } from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['lists', 'users'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
|
kind: 'write:account',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchList: {
|
||||||
|
message: 'No such list.',
|
||||||
|
code: 'NO_SUCH_LIST',
|
||||||
|
id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '588e7f72-c744-4a61-b180-d354e912bda2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
listId: { type: 'string', format: 'misskey:id' },
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
withReplies: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
required: ['listId', 'userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userListsRepository)
|
||||||
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
|
private userListService: UserListService,
|
||||||
|
private getterService: GetterService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
// Fetch the list
|
||||||
|
const userList = await this.userListsRepository.findOneBy({
|
||||||
|
id: ps.listId,
|
||||||
|
userId: me.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userList == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the user
|
||||||
|
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
||||||
|
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.userListService.updateMembership(user, userList, {
|
||||||
|
withReplies: ps.withReplies,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,19 +5,19 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'notes'],
|
tags: ['users', 'notes'],
|
||||||
|
|
||||||
description: 'Show all notes that this user created.',
|
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@ -50,9 +50,6 @@ export const paramDef = {
|
|||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
includeMyRenotes: { type: 'boolean', default: true },
|
includeMyRenotes: { type: 'boolean', default: true },
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
fileType: { type: 'array', items: {
|
|
||||||
type: 'string',
|
|
||||||
} },
|
|
||||||
excludeNsfw: { type: 'boolean', default: false },
|
excludeNsfw: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: ['userId'],
|
required: ['userId'],
|
||||||
@ -61,87 +58,78 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Lookup user
|
let timeline: MiNote[] = [];
|
||||||
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
|
||||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region Construct query
|
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
let noteIdsRes: [string, string[]][] = [];
|
||||||
.andWhere('note.userId = :userId', { userId: user.id })
|
let repliesNoteIdsRes: [string, string[]][] = [];
|
||||||
|
|
||||||
|
if (!ps.sinceId && !ps.sinceDate) {
|
||||||
|
[noteIdsRes, repliesNoteIdsRes] = await Promise.all([
|
||||||
|
this.redisForTimelines.xrevrange(
|
||||||
|
ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit),
|
||||||
|
ps.withReplies
|
||||||
|
? this.redisForTimelines.xrevrange(
|
||||||
|
`userTimelineWithReplies:${ps.userId}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', limit)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let noteIds = Array.from(new Set([
|
||||||
|
...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
|
||||||
|
...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
|
||||||
|
]));
|
||||||
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFollowing = me ? Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId) : false;
|
||||||
|
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
.leftJoinAndSelect('note.channel', 'channel')
|
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
query.andWhere(new Brackets(qb => {
|
timeline = await query.getMany();
|
||||||
qb.orWhere('note.channelId IS NULL');
|
|
||||||
qb.orWhere('channel.isSensitive = false');
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
timeline = timeline.filter(note => {
|
||||||
if (me) {
|
if (note.renoteId) {
|
||||||
this.queryService.generateMutedUserQuery(query, me, user);
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
if (ps.withRenotes === false) return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.fileType != null) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
for (const type of ps.fileType!) {
|
|
||||||
const i = ps.fileType!.indexOf(type);
|
|
||||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
if (ps.excludeNsfw) {
|
|
||||||
query.andWhere('note.cw IS NULL');
|
|
||||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!ps.withReplies) {
|
if (note.visibility === 'followers' && !isFollowing) return false;
|
||||||
query.andWhere('note.replyId IS NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
return true;
|
||||||
query.andWhere(new Brackets(qb => {
|
});
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.userId != :userId', { userId: user.id });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,7 @@ import type { NoteReadService } from '@/core/NoteReadService.js';
|
|||||||
import type { NotificationService } from '@/core/NotificationService.js';
|
import type { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiUserProfile } from '@/models/_.js';
|
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { ChannelsService } from './ChannelsService.js';
|
import type { ChannelsService } from './ChannelsService.js';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
@ -30,7 +30,7 @@ export default class Connection {
|
|||||||
private subscribingNotes: any = {};
|
private subscribingNotes: any = {};
|
||||||
private cachedNotes: Packed<'Note'>[] = [];
|
private cachedNotes: Packed<'Note'>[] = [];
|
||||||
public userProfile: MiUserProfile | null = null;
|
public userProfile: MiUserProfile | null = null;
|
||||||
public following: Set<string> = new Set();
|
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||||
public followingChannels: Set<string> = new Set();
|
public followingChannels: Set<string> = new Set();
|
||||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||||
|
@ -18,7 +18,6 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
public readonly chName = 'globalTimeline';
|
public readonly chName = 'globalTimeline';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = false;
|
public static requireCredential = false;
|
||||||
private withReplies: boolean;
|
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -38,7 +37,6 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||||
if (!policies.gtlAvailable) return;
|
if (!policies.gtlAvailable) return;
|
||||||
|
|
||||||
this.withReplies = params.withReplies ?? false;
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
@ -64,7 +62,7 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && !this.withReplies) {
|
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
@ -82,13 +80,6 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
|
||||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
@ -16,7 +16,6 @@ class HomeTimelineChannel extends Channel {
|
|||||||
public readonly chName = 'homeTimeline';
|
public readonly chName = 'homeTimeline';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = true;
|
public static requireCredential = true;
|
||||||
private withReplies: boolean;
|
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -31,7 +30,6 @@ class HomeTimelineChannel extends Channel {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
this.withReplies = params.withReplies ?? false;
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
this.subscriber.on('notesStream', this.onNote);
|
this.subscriber.on('notesStream', this.onNote);
|
||||||
@ -43,7 +41,7 @@ class HomeTimelineChannel extends Channel {
|
|||||||
if (!this.followingChannels.has(note.channelId)) return;
|
if (!this.followingChannels.has(note.channelId)) return;
|
||||||
} else {
|
} else {
|
||||||
// その投稿のユーザーをフォローしていなかったら弾く
|
// その投稿のユーザーをフォローしていなかったら弾く
|
||||||
if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
|
if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
@ -73,7 +71,7 @@ class HomeTimelineChannel extends Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && !this.withReplies) {
|
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
@ -88,13 +86,6 @@ class HomeTimelineChannel extends Channel {
|
|||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
|
||||||
if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return;
|
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
@ -18,7 +18,6 @@ class HybridTimelineChannel extends Channel {
|
|||||||
public readonly chName = 'hybridTimeline';
|
public readonly chName = 'hybridTimeline';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = true;
|
public static requireCredential = true;
|
||||||
private withReplies: boolean;
|
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -38,7 +37,6 @@ class HybridTimelineChannel extends Channel {
|
|||||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||||
if (!policies.ltlAvailable) return;
|
if (!policies.ltlAvailable) return;
|
||||||
|
|
||||||
this.withReplies = params.withReplies ?? false;
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
@ -53,7 +51,7 @@ class HybridTimelineChannel extends Channel {
|
|||||||
// フォローしているチャンネルの投稿 の場合だけ
|
// フォローしているチャンネルの投稿 の場合だけ
|
||||||
if (!(
|
if (!(
|
||||||
(note.channelId == null && this.user!.id === note.userId) ||
|
(note.channelId == null && this.user!.id === note.userId) ||
|
||||||
(note.channelId == null && this.following.has(note.userId)) ||
|
(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
|
||||||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
|
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
|
||||||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||||
)) return;
|
)) return;
|
||||||
@ -85,7 +83,7 @@ class HybridTimelineChannel extends Channel {
|
|||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && !this.withReplies) {
|
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
@ -100,13 +98,6 @@ class HybridTimelineChannel extends Channel {
|
|||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
|
||||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
@ -17,7 +17,6 @@ class LocalTimelineChannel extends Channel {
|
|||||||
public readonly chName = 'localTimeline';
|
public readonly chName = 'localTimeline';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = false;
|
public static requireCredential = false;
|
||||||
private withReplies: boolean;
|
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -37,7 +36,6 @@ class LocalTimelineChannel extends Channel {
|
|||||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||||
if (!policies.ltlAvailable) return;
|
if (!policies.ltlAvailable) return;
|
||||||
|
|
||||||
this.withReplies = params.withReplies ?? false;
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
@ -64,7 +62,7 @@ class LocalTimelineChannel extends Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && this.user && !this.withReplies) {
|
if (note.reply && this.user && !this.following[note.userId]?.withReplies) {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||||
@ -79,13 +77,6 @@ class LocalTimelineChannel extends Channel {
|
|||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
|
||||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
@ -18,12 +18,12 @@ class UserListChannel extends Channel {
|
|||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = false;
|
public static requireCredential = false;
|
||||||
private listId: string;
|
private listId: string;
|
||||||
public listUsers: MiUser['id'][] = [];
|
public membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||||
private listUsersClock: NodeJS.Timeout;
|
private listUsersClock: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
|
||||||
id: string,
|
id: string,
|
||||||
@ -58,19 +58,25 @@ class UserListChannel extends Channel {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async updateListUsers() {
|
private async updateListUsers() {
|
||||||
const users = await this.userListJoiningsRepository.find({
|
const memberships = await this.userListMembershipsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userListId: this.listId,
|
userListId: this.listId,
|
||||||
},
|
},
|
||||||
select: ['userId'],
|
select: ['userId'],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listUsers = users.map(x => x.userId);
|
const membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||||
|
for (const membership of memberships) {
|
||||||
|
membershipsMap[membership.userId] = {
|
||||||
|
withReplies: membership.withReplies,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.membershipsMap = membershipsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
if (!this.listUsers.includes(note.userId)) return;
|
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||||
|
|
||||||
if (['followers', 'specified'].includes(note.visibility)) {
|
if (['followers', 'specified'].includes(note.visibility)) {
|
||||||
note = await this.noteEntityService.pack(note.id, this.user, {
|
note = await this.noteEntityService.pack(note.id, this.user, {
|
||||||
@ -95,6 +101,13 @@ class UserListChannel extends Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 関係ない返信は除外
|
||||||
|
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
|
||||||
|
const reply = note.reply;
|
||||||
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
@ -124,8 +137,8 @@ export class UserListChannelService {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
) {
|
) {
|
||||||
@ -135,7 +148,7 @@ export class UserListChannelService {
|
|||||||
public create(id: string, connection: Channel['connection']): UserListChannel {
|
public create(id: string, connection: Channel['connection']): UserListChannel {
|
||||||
return new UserListChannel(
|
return new UserListChannel(
|
||||||
this.userListsRepository,
|
this.userListsRepository,
|
||||||
this.userListJoiningsRepository,
|
this.userListMembershipsRepository,
|
||||||
this.noteEntityService,
|
this.noteEntityService,
|
||||||
id,
|
id,
|
||||||
connection,
|
connection,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
|
import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
@ -42,6 +42,9 @@ describe('Renote Mute', () => {
|
|||||||
const carolRenote = await post(carol, { renoteId: bobNote.id });
|
const carolRenote = await post(carol, { renoteId: bobNote.id });
|
||||||
const carolNote = await post(carol, { text: 'hi' });
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
|
// redisに追加されるのを待つ
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
const res = await api('/notes/local-timeline', {}, alice);
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
@ -56,6 +59,9 @@ describe('Renote Mute', () => {
|
|||||||
const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' });
|
const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' });
|
||||||
const carolNote = await post(carol, { text: 'hi' });
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
|
// redisに追加されるのを待つ
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
const res = await api('/notes/local-timeline', {}, alice);
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
819
packages/backend/test/e2e/timelines.ts
Normal file
819
packages/backend/test/e2e/timelines.ts
Normal file
@ -0,0 +1,819 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js';
|
||||||
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
|
function genHost() {
|
||||||
|
return randomString() + '.example.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await startServer();
|
||||||
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Timelines', () => {
|
||||||
|
describe('Home TL', () => {
|
||||||
|
test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
|
||||||
|
const [alice] = await Promise.all([signup()]);
|
||||||
|
|
||||||
|
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/create', { userId: carol.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/create', { userId: carol.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('自分の他人への返信が含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { renoteId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { renoteId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {
|
||||||
|
withRenotes: false,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {
|
||||||
|
withRenotes: false,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/mute/create', { userId: carol.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
await api('/mute/create', { userId: carol.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const [bobFile, carolFile] = await Promise.all([
|
||||||
|
uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'),
|
||||||
|
uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'),
|
||||||
|
]);
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { fileIds: [bobFile.id] });
|
||||||
|
const carolNote1 = await post(carol, { text: 'hi' });
|
||||||
|
const carolNote2 = await post(carol, { fileIds: [carolFile.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/timeline', { withFiles: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false);
|
||||||
|
}, 1000 * 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Local TL', () => {
|
||||||
|
test.concurrent('visibility: home なノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 含まれても良いと思うけど実装が面倒なので含まれない
|
||||||
|
test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: carol.id,
|
||||||
|
}, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/mute/create', { userId: carol.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/mute/create', { userId: carol.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
await api('/mute/create', { userId: carol.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { fileIds: [file.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', { withFiles: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
}, 1000 * 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Social TL', () => {
|
||||||
|
test.concurrent('ローカルユーザーのノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { fileIds: [file.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/hybrid-timeline', { withFiles: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
}, 1000 * 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User List TL', () => {
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 未実装
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれるが隠される', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||||
|
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||||
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { fileIds: [file.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
}, 1000 * 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User TL', () => {
|
||||||
|
test.concurrent('ノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/following/create', { userId: bob.id }, alice);
|
||||||
|
await sleep(1000);
|
||||||
|
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
|
||||||
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
|
||||||
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
|
const bobNote2 = await post(bob, { fileIds: [file.id] });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||||
|
}, 1000 * 10);
|
||||||
|
|
||||||
|
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
await api('/mute/create', { userId: bob.id }, alice);
|
||||||
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
|
await sleep(100); // redisに追加されるのを待つ
|
||||||
|
|
||||||
|
const res = await api('/users/notes', { userId: bob.id }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: リノートミュート済みユーザーのテスト
|
||||||
|
// TODO: ページネーションのテスト
|
||||||
|
});
|
@ -38,23 +38,10 @@ describe('users/notes', () => {
|
|||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ファイルタイプ指定 (jpg)', async () => {
|
test('withFiles', async () => {
|
||||||
const res = await api('/users/notes', {
|
const res = await api('/users/notes', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
fileType: ['image/jpeg'],
|
withFiles: true,
|
||||||
}, alice);
|
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
|
||||||
assert.strictEqual(res.body.length, 2);
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ファイルタイプ指定 (jpg or png)', async () => {
|
|
||||||
const res = await api('/users/notes', {
|
|
||||||
userId: alice.id,
|
|
||||||
fileType: ['image/jpeg', 'image/png'],
|
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -133,6 +133,7 @@ describe('ユーザー', () => {
|
|||||||
isMuted: user.isMuted ?? false,
|
isMuted: user.isMuted ?? false,
|
||||||
isRenoteMuted: user.isRenoteMuted ?? false,
|
isRenoteMuted: user.isRenoteMuted ?? false,
|
||||||
notify: user.notify ?? 'none',
|
notify: user.notify ?? 'none',
|
||||||
|
withReplies: user.withReplies ?? false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,6 +92,9 @@ describe('ActivityPub', () => {
|
|||||||
const metaInitial = {
|
const metaInitial = {
|
||||||
cacheRemoteFiles: true,
|
cacheRemoteFiles: true,
|
||||||
cacheRemoteSensitiveFiles: true,
|
cacheRemoteSensitiveFiles: true,
|
||||||
|
perUserHomeTimelineCacheMax: 100,
|
||||||
|
perLocalUserUserTimelineCacheMax: 100,
|
||||||
|
perRemoteUserUserTimelineCacheMax: 100,
|
||||||
blockedHosts: [] as string[],
|
blockedHosts: [] as string[],
|
||||||
sensitiveWords: [] as string[],
|
sensitiveWords: [] as string[],
|
||||||
} as MiMeta;
|
} as MiMeta;
|
||||||
|
@ -99,9 +99,17 @@ export const relativeFetch = async (path: string, init?: RequestInit | undefined
|
|||||||
return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init);
|
return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) {
|
||||||
|
let randomString = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
randomString += chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
|
|
||||||
export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']>): Promise<NonNullable<misskey.Endpoints['signup']['res']>> => {
|
export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']>): Promise<NonNullable<misskey.Endpoints['signup']['res']>> => {
|
||||||
const q = Object.assign({
|
const q = Object.assign({
|
||||||
username: 'test',
|
username: randomString(),
|
||||||
password: 'test',
|
password: 'test',
|
||||||
}, params);
|
}, params);
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ import { deepClone } from '@/scripts/clone.js';
|
|||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||||
@ -211,7 +211,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
|
|||||||
const isLong = shouldCollapsed(appearNote);
|
const isLong = shouldCollapsed(appearNote);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
const translation = ref<any>(null);
|
const translation = ref<any>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
|
@ -93,9 +93,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div :class="$style.noteFooterInfo">
|
<div :class="$style.noteFooterInfo">
|
||||||
<div v-if="appearNote.updatedAt">
|
|
||||||
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
|
|
||||||
</div>
|
|
||||||
<MkA :to="notePage(appearNote)">
|
<MkA :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
@ -214,7 +211,7 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
|||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
@ -258,7 +255,7 @@ let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note
|
|||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||||
|
@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
|
|
||||||
<MkA :to="notePage(note)">
|
<MkA :to="notePage(note)">
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
@ -49,9 +49,9 @@ import { notePage } from '@/filters/note.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { userPage } from "@/filters/user";
|
import { userPage } from '@/filters/user.js';
|
||||||
import { checkWordMute } from "@/scripts/check-word-mute";
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
@ -63,7 +63,7 @@ const props = withDefaults(defineProps<{
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const muted = ref(checkWordMute(props.note, $i, defaultStore.state.mutedWords));
|
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||||
|
|
||||||
let showContent = $ref(false);
|
let showContent = $ref(false);
|
||||||
let replies: Misskey.entities.Note[] = $ref([]);
|
let replies: Misskey.entities.Note[] = $ref([]);
|
||||||
|
@ -143,7 +143,6 @@ const props = withDefaults(defineProps<{
|
|||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
freezeAfterPosted?: boolean;
|
freezeAfterPosted?: boolean;
|
||||||
updateMode?: boolean;
|
|
||||||
}>(), {
|
}>(), {
|
||||||
initialVisibleUsers: () => [],
|
initialVisibleUsers: () => [],
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@ -710,7 +709,6 @@ async function post(ev?: MouseEvent) {
|
|||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
|
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
|
||||||
reactionAcceptance,
|
reactionAcceptance,
|
||||||
noteId: props.updateMode ? props.initialNote?.id : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (withHashtags && hashtags && hashtags.trim() !== '') {
|
if (withHashtags && hashtags && hashtags.trim() !== '') {
|
||||||
@ -733,7 +731,7 @@ async function post(ev?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
posting = true;
|
posting = true;
|
||||||
os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
|
os.api('notes/create', postData, token).then(() => {
|
||||||
if (props.freezeAfterPosted) {
|
if (props.freezeAfterPosted) {
|
||||||
posted = true;
|
posted = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -30,7 +30,6 @@ const props = defineProps<{
|
|||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
updateMode?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -23,11 +23,9 @@ const props = withDefaults(defineProps<{
|
|||||||
role?: string;
|
role?: string;
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
|
||||||
onlyFiles?: boolean;
|
onlyFiles?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
withReplies: false,
|
|
||||||
onlyFiles: false,
|
onlyFiles: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,12 +68,10 @@ if (props.src === 'antenna') {
|
|||||||
endpoint = 'notes/timeline';
|
endpoint = 'notes/timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('homeTimeline', {
|
connection = stream.useChannel('homeTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
@ -85,12 +81,10 @@ if (props.src === 'antenna') {
|
|||||||
endpoint = 'notes/local-timeline';
|
endpoint = 'notes/local-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('localTimeline', {
|
connection = stream.useChannel('localTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
@ -98,12 +92,10 @@ if (props.src === 'antenna') {
|
|||||||
endpoint = 'notes/hybrid-timeline';
|
endpoint = 'notes/hybrid-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
@ -111,12 +103,10 @@ if (props.src === 'antenna') {
|
|||||||
endpoint = 'notes/global-timeline';
|
endpoint = 'notes/global-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('globalTimeline', {
|
connection = stream.useChannel('globalTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
@ -140,13 +130,11 @@ if (props.src === 'antenna') {
|
|||||||
endpoint = 'notes/user-list-timeline';
|
endpoint = 'notes/user-list-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('userList', {
|
connection = stream.useChannel('userList', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
});
|
});
|
||||||
|
@ -61,7 +61,6 @@ export const ROLE_POLICIES = [
|
|||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
'canEditNote',
|
|
||||||
'canInvite',
|
'canInvite',
|
||||||
'inviteLimit',
|
'inviteLimit',
|
||||||
'inviteLimitCycle',
|
'inviteLimitCycle',
|
||||||
|
@ -287,6 +287,7 @@ const patrons = [
|
|||||||
'kino3277',
|
'kino3277',
|
||||||
'美少女JKぐーちゃん',
|
'美少女JKぐーちゃん',
|
||||||
'てば',
|
'てば',
|
||||||
|
'たっくん',
|
||||||
];
|
];
|
||||||
|
|
||||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||||
|
81
packages/frontend/src/pages/admin/external-services.vue
Normal file
81
packages/frontend/src/pages/admin/external-services.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSection>
|
||||||
|
<template #label>DeepL Translation</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkInput v-model="deeplAuthKey">
|
||||||
|
<template #prefix><i class="ti ti-key"></i></template>
|
||||||
|
<template #label>DeepL Auth Key</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkSwitch v-model="deeplIsPro">
|
||||||
|
<template #label>Pro account</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
</FormSuspense>
|
||||||
|
</MkSpacer>
|
||||||
|
<template #footer>
|
||||||
|
<div :class="$style.footer">
|
||||||
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||||
|
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
</MkSpacer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { fetchInstance } from '@/instance.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
|
||||||
|
let deeplAuthKey: string = $ref('');
|
||||||
|
let deeplIsPro: boolean = $ref(false);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const meta = await os.api('admin/meta');
|
||||||
|
deeplAuthKey = meta.deeplAuthKey;
|
||||||
|
deeplIsPro = meta.deeplIsPro;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
deeplAuthKey,
|
||||||
|
deeplIsPro,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: i18n.ts.externalServices,
|
||||||
|
icon: 'ti ti-link',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.footer {
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
}
|
||||||
|
</style>
|
@ -198,6 +198,11 @@ const menuDef = $computed(() => [{
|
|||||||
text: i18n.ts.proxyAccount,
|
text: i18n.ts.proxyAccount,
|
||||||
to: '/admin/proxy-account',
|
to: '/admin/proxy-account',
|
||||||
active: currentPage?.route.name === 'proxy-account',
|
active: currentPage?.route.name === 'proxy-account',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-link',
|
||||||
|
text: i18n.ts.externalServices,
|
||||||
|
to: '/admin/external-services',
|
||||||
|
active: currentPage?.route.name === 'external-services',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-adjustments',
|
icon: 'ti ti-adjustments',
|
||||||
text: i18n.ts.other,
|
text: i18n.ts.other,
|
||||||
|
@ -160,26 +160,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
|
|
||||||
<template #suffix>
|
|
||||||
<span v-if="role.policies.canEditNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
|
||||||
<span v-else>{{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
|
||||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canEditNote)"></i></span>
|
|
||||||
</template>
|
|
||||||
<div class="_gaps">
|
|
||||||
<MkSwitch v-model="role.policies.canEditNote.useDefault" :readonly="readonly">
|
|
||||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkSwitch v-model="role.policies.canEditNote.value" :disabled="role.policies.canEditNote.useDefault" :readonly="readonly">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkRange v-model="role.policies.canEditNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
|
||||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
|
||||||
</MkRange>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@ -48,14 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
|
|
||||||
<template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
|
||||||
<MkSwitch v-model="policies.canEditNote">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
@ -81,16 +81,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>DeepL Translation</template>
|
<template #label>Timeline caching</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkInput v-model="deeplAuthKey">
|
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
<template #label>perLocalUserUserTimelineCacheMax</template>
|
||||||
<template #label>DeepL Auth Key</template>
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number">
|
||||||
|
<template #label>perRemoteUserUserTimelineCacheMax</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="perUserHomeTimelineCacheMax" type="number">
|
||||||
|
<template #label>perUserHomeTimelineCacheMax</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="perUserListTimelineCacheMax" type="number">
|
||||||
|
<template #label>perUserListTimelineCacheMax</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSwitch v-model="deeplIsPro">
|
|
||||||
<template #label>Pro account</template>
|
|
||||||
</MkSwitch>
|
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
@ -133,8 +141,10 @@ let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
|||||||
let enableServiceWorker: boolean = $ref(false);
|
let enableServiceWorker: boolean = $ref(false);
|
||||||
let swPublicKey: any = $ref(null);
|
let swPublicKey: any = $ref(null);
|
||||||
let swPrivateKey: any = $ref(null);
|
let swPrivateKey: any = $ref(null);
|
||||||
let deeplAuthKey: string = $ref('');
|
let perLocalUserUserTimelineCacheMax: number = $ref(0);
|
||||||
let deeplIsPro: boolean = $ref(false);
|
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
|
||||||
|
let perUserHomeTimelineCacheMax: number = $ref(0);
|
||||||
|
let perUserListTimelineCacheMax: number = $ref(0);
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
@ -149,8 +159,10 @@ async function init(): Promise<void> {
|
|||||||
enableServiceWorker = meta.enableServiceWorker;
|
enableServiceWorker = meta.enableServiceWorker;
|
||||||
swPublicKey = meta.swPublickey;
|
swPublicKey = meta.swPublickey;
|
||||||
swPrivateKey = meta.swPrivateKey;
|
swPrivateKey = meta.swPrivateKey;
|
||||||
deeplAuthKey = meta.deeplAuthKey;
|
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
|
||||||
deeplIsPro = meta.deeplIsPro;
|
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
|
||||||
|
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
|
||||||
|
perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(): void {
|
function save(): void {
|
||||||
@ -166,8 +178,10 @@ function save(): void {
|
|||||||
enableServiceWorker,
|
enableServiceWorker,
|
||||||
swPublicKey,
|
swPublicKey,
|
||||||
swPrivateKey,
|
swPrivateKey,
|
||||||
deeplAuthKey,
|
perLocalUserUserTimelineCacheMax,
|
||||||
deeplIsPro,
|
perRemoteUserUserTimelineCacheMax,
|
||||||
|
perUserHomeTimelineCacheMax,
|
||||||
|
perUserListTimelineCacheMax,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
@ -29,16 +29,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||||
<div v-for="user in users" :key="user.id" :class="$style.userItem">
|
|
||||||
<MkA :class="$style.userItemBody" :to="`${userPage(user)}`">
|
<MkPagination ref="paginationEl" :pagination="membershipsPagination">
|
||||||
<MkUserCardMini :user="user"/>
|
<template #default="{ items }">
|
||||||
</MkA>
|
<div class="_gaps_s">
|
||||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
<div v-for="item in items" :key="item.id">
|
||||||
</div>
|
<div :class="$style.userItem">
|
||||||
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
|
<MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`">
|
||||||
{{ i18n.ts.loadMore }}
|
<MkUserCardMini :user="item.user"/>
|
||||||
</MkButton>
|
</MkA>
|
||||||
<MkLoading v-if="fetching" class="loading"/>
|
<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button>
|
||||||
|
<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
@ -59,9 +65,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { userListsCache } from '@/cache';
|
import { userListsCache } from '@/cache.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enableInfiniteScroll,
|
enableInfiniteScroll,
|
||||||
} = defaultStore.reactiveState;
|
} = defaultStore.reactiveState;
|
||||||
@ -70,40 +78,25 @@ const props = defineProps<{
|
|||||||
listId: string;
|
listId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const FETCH_USERS_LIMIT = 20;
|
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
let list = $ref<Misskey.entities.UserList | null>(null);
|
let list = $ref<Misskey.entities.UserList | null>(null);
|
||||||
let users = $ref<Misskey.entities.UserLite[]>([]);
|
|
||||||
let queueUserIds = $ref<string[]>([]);
|
|
||||||
let fetching = $ref(true);
|
|
||||||
const isPublic = ref(false);
|
const isPublic = ref(false);
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
|
const membershipsPagination = {
|
||||||
|
endpoint: 'users/lists/get-memberships' as const,
|
||||||
|
limit: 30,
|
||||||
|
params: computed(() => ({
|
||||||
|
listId: props.listId,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
function fetchList() {
|
function fetchList() {
|
||||||
fetching = true;
|
|
||||||
os.api('users/lists/show', {
|
os.api('users/lists/show', {
|
||||||
listId: props.listId,
|
listId: props.listId,
|
||||||
}).then(_list => {
|
}).then(_list => {
|
||||||
list = _list;
|
list = _list;
|
||||||
name.value = list.name;
|
name.value = list.name;
|
||||||
isPublic.value = list.isPublic;
|
isPublic.value = list.isPublic;
|
||||||
queueUserIds = list.userIds;
|
|
||||||
|
|
||||||
return fetchMoreUsers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchMoreUsers() {
|
|
||||||
if (!list) return;
|
|
||||||
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
|
|
||||||
fetching = true;
|
|
||||||
os.api('users/show', {
|
|
||||||
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
|
|
||||||
}).then(_users => {
|
|
||||||
users = users.concat(_users);
|
|
||||||
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
|
|
||||||
}).finally(() => {
|
|
||||||
fetching = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,12 +107,12 @@ function addUser() {
|
|||||||
listId: list.id,
|
listId: list.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
users.push(user);
|
paginationEl.value.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeUser(user, ev) {
|
async function removeUser(item, ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.remove,
|
text: i18n.ts.remove,
|
||||||
icon: 'ti ti-x',
|
icon: 'ti ti-x',
|
||||||
@ -128,9 +121,28 @@ async function removeUser(user, ev) {
|
|||||||
if (!list) return;
|
if (!list) return;
|
||||||
os.api('users/lists/pull', {
|
os.api('users/lists/pull', {
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
userId: user.id,
|
userId: item.userId,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
users = users.filter(x => x.id !== user.id);
|
paginationEl.value.removeItem(item.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showMembershipMenu(item, ev) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
|
||||||
|
icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
|
||||||
|
action: async () => {
|
||||||
|
os.api('users/lists/update-membership', {
|
||||||
|
listId: list.id,
|
||||||
|
userId: item.userId,
|
||||||
|
withReplies: !item.withReplies,
|
||||||
|
}).then(() => {
|
||||||
|
paginationEl.value.updateItem(item.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
withReplies: !item.withReplies,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
@ -202,6 +214,12 @@ definePageMetadata(computed(() => list ? {
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
@ -83,6 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #value><code class="_monospace">{{ code }}</code></template>
|
<template #value><code class="_monospace">{{ code }}</code></template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
@ -108,6 +110,7 @@ import * as os from '@/os.js';
|
|||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
twoFactorData: {
|
twoFactorData: {
|
||||||
@ -143,6 +146,16 @@ async function tokenDone() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadBackupCodes() {
|
||||||
|
if (backupCodes.value !== undefined) {
|
||||||
|
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
|
||||||
|
const dummya = document.createElement('a');
|
||||||
|
dummya.href = URL.createObjectURL(txtBlob);
|
||||||
|
dummya.download = `${$i?.username}-2fa-backup-codes.txt`;
|
||||||
|
dummya.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function allDone() {
|
function allDone() {
|
||||||
dialog.value.close();
|
dialog.value.close();
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkTab v-model="tab">
|
<MkFolder>
|
||||||
<option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option>
|
<template #icon><i class="ti ti-repeat-off"></i></template>
|
||||||
<option value="mute">{{ i18n.ts.mutedUsers }}</option>
|
<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
|
||||||
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
|
||||||
</MkTab>
|
|
||||||
|
|
||||||
<div v-if="tab === 'renoteMute'">
|
|
||||||
<MkPagination :pagination="renoteMutingPagination">
|
<MkPagination :pagination="renoteMutingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
@ -37,9 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.mutedUsers }}</template>
|
||||||
|
|
||||||
<div v-else-if="tab === 'mute'">
|
|
||||||
<MkPagination :pagination="mutingPagination">
|
<MkPagination :pagination="mutingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
@ -67,9 +67,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-ban"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.blockedUsers }}</template>
|
||||||
|
|
||||||
<div v-else-if="tab === 'block'">
|
|
||||||
<MkPagination :pagination="blockingPagination">
|
<MkPagination :pagination="blockingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
@ -97,24 +100,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
|
||||||
import FormLink from '@/components/form/link.vue';
|
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
let tab = $ref('renoteMute');
|
|
||||||
|
|
||||||
const renoteMutingPagination = {
|
const renoteMutingPagination = {
|
||||||
endpoint: 'renote-mute/list' as const,
|
endpoint: 'renote-mute/list' as const,
|
||||||
|
@ -5,29 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkTab v-model="tab">
|
|
||||||
<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
|
|
||||||
<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
|
|
||||||
</MkTab>
|
|
||||||
<div>
|
<div>
|
||||||
<div v-show="tab === 'soft'" class="_gaps_m">
|
<MkTextarea v-model="mutedWords">
|
||||||
<MkInfo>{{ i18n.ts._wordMute.softDescription }}</MkInfo>
|
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||||
<MkTextarea v-model="softMutedWords">
|
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
</MkTextarea>
|
||||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
|
||||||
</MkTextarea>
|
|
||||||
</div>
|
|
||||||
<div v-show="tab === 'hard'" class="_gaps_m">
|
|
||||||
<MkInfo>{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
|
|
||||||
<MkTextarea v-model="hardMutedWords">
|
|
||||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
|
||||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
|
||||||
</MkTextarea>
|
|
||||||
<MkKeyValue v-if="hardWordMutedNotesCount != null">
|
|
||||||
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
|
|
||||||
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
|
|
||||||
</MkKeyValue>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
@ -56,25 +38,15 @@ const render = (mutedWords) => mutedWords.map(x => {
|
|||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
const tab = ref('soft');
|
const tab = ref('soft');
|
||||||
const softMutedWords = ref(render(defaultStore.state.mutedWords));
|
const mutedWords = ref(render($i!.mutedWords));
|
||||||
const hardMutedWords = ref(render($i!.mutedWords));
|
|
||||||
const hardWordMutedNotesCount = ref(null);
|
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
|
|
||||||
os.api('i/get-word-muted-notes-count', {}).then(response => {
|
watch(mutedWords, () => {
|
||||||
hardWordMutedNotesCount.value = response?.count;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(softMutedWords, () => {
|
|
||||||
changed.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(hardMutedWords, () => {
|
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const parseMutes = (mutes, tab) => {
|
const parseMutes = (mutes) => {
|
||||||
// split into lines, remove empty lines and unnecessary whitespace
|
// split into lines, remove empty lines and unnecessary whitespace
|
||||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||||
|
|
||||||
@ -92,7 +64,7 @@ async function save() {
|
|||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: i18n.ts.regexpError,
|
title: i18n.ts.regexpError,
|
||||||
text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + '\n' + err.toString(),
|
text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||||
});
|
});
|
||||||
// re-throw error so these invalid settings are not saved
|
// re-throw error so these invalid settings are not saved
|
||||||
throw err;
|
throw err;
|
||||||
@ -105,18 +77,16 @@ async function save() {
|
|||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
let softMutes, hardMutes;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
|
parsed = parseMutes(mutedWords.value);
|
||||||
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// already displayed error message in parseMutes
|
// already displayed error message in parseMutes
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultStore.set('mutedWords', softMutes);
|
|
||||||
await os.api('i/update', {
|
await os.api('i/update', {
|
||||||
mutedWords: hardMutes,
|
mutedWords: parsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user