Compare commits

..

33 Commits

Author SHA1 Message Date
syuilo
adf9d9c969 2023.10.0-beta.4 2023-10-06 18:01:19 +09:00
syuilo
8c663f65a8 clean up 2023-10-06 17:40:46 +09:00
syuilo
481ca4ec03 add more test 2023-10-06 17:36:54 +09:00
syuilo
e6ca53c5e1 update rollup to 4.0.0 2023-10-06 17:36:43 +09:00
syuilo
95dc70021f update deps 2023-10-06 17:19:17 +09:00
syuilo
fd3295eba4 Update CHANGELOG.md 2023-10-06 17:13:13 +09:00
syuilo
a76cebd897 Update CHANGELOG.md 2023-10-06 17:11:59 +09:00
syuilo
7d289c1b77 refactor 2023-10-06 17:01:06 +09:00
syuilo
0bdbdba9f8 refactor 2023-10-06 16:58:38 +09:00
syuilo
4489ca3c74 refactor 2023-10-06 16:28:21 +09:00
syuilo
87416710c3 enhance(backend): some tweaks 2023-10-06 16:17:29 +09:00
syuilo
132b01461d refactor 2023-10-06 16:10:59 +09:00
syuilo
dab205edb8 enhance(backend): improve featured system 2023-10-06 14:24:25 +09:00
syuilo
e4dcab8671 chore(backend): response isHibernated in admin/show-user 2023-10-05 21:35:23 +09:00
syuilo
780721e9a2 clean up 2023-10-05 20:34:50 +09:00
anatawa12
ee483f2dee Disallow renote of direct note (#11970)
* chore: renoteに関するチェックをまとめる

* fix: ダイレクト投稿をrenoteできる

* fix(frontend): 自分のダイレクト投稿をrenoteできる

* docs(changelog): ダイレクト投稿をリノートできてしまう

* fix lint

* chore(backend): visibilityに関するエラーをApi Errorとして返す
2023-10-05 17:03:50 +09:00
syuilo
2a7bc847b0 2023.10.0-beta.3 2023-10-05 11:00:46 +09:00
syuilo
2333bdb98a fix(backend): sinceIdやsinceDateを利用してTLを取得することができない
Fix #11961
2023-10-05 10:23:58 +09:00
syuilo
979741ce09 tweak ui 2023-10-05 10:03:13 +09:00
syuilo
5b00fa6f82 enhance(backend): users/notesでチャンネル投稿を含めるオプション
Resolve #11965
2023-10-05 09:48:45 +09:00
syuilo
d2bb35bcf3 add more tests 2023-10-05 09:31:03 +09:00
Camilla Ett
e4ade46a2d fix(frontend): クライアント設定から13.7.0で削除されたチャット機能に関するサウンド設定を削除 (#11912)
* fix(frontend): クライアント設定から13.7.0で削除されたチャット機能に関するサウンド設定を削除

* fix(frontend): 各localesからsfx/chat sfx/chatbgを削除
2023-10-04 20:17:05 +09:00
syuilo
89e4f28d06 enhance(frontend): tweak ui 2023-10-04 20:03:52 +09:00
syuilo
0dbf5175df enhance: improve moderation log 2023-10-04 19:55:35 +09:00
syuilo
55c14aec2c perf(backend): delete useless indexes 2023-10-04 16:50:06 +09:00
syuilo
fb63fc1213 tweak tl 2023-10-04 16:48:34 +09:00
syuilo
ca515d5a7e tweak timelines 2023-10-04 16:43:24 +09:00
syuilo
6ebea82dba add more test 2023-10-04 16:12:44 +09:00
syuilo
05d1f5e564 add more test 2023-10-04 15:33:02 +09:00
syuilo
ee70f05a86 fix test 2023-10-04 15:29:09 +09:00
syuilo
fb6a5c8356 add more test 2023-10-04 14:47:45 +09:00
syuilo
a997b7bdcc fix test 2023-10-04 14:45:46 +09:00
syuilo
bcbcaa9c60 🎨 2023-10-04 12:56:57 +09:00
75 changed files with 1430 additions and 1118 deletions

View File

@@ -14,8 +14,9 @@
## 2023.10.0
### NOTE
- muted_noteテーブルは使われなくなったため手動で削除を行ってください。
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
- アップデート後、アップデートより前の時点にTLを遡ることはできません
- アップデート後であっても、今後のアップデートで2023.10.0以前のTLに遡れるようになる可能性はあります
### Changes
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
@@ -25,13 +26,20 @@
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Fix: ダイレクト投稿をリノートできてしまう問題を修正
- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正
### Client
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
### Server
- Enhance: タイムライン取得時のパフォーマンスを改善
- Enhance: タイムライン取得時のパフォーマンスを大幅に向上
- Enhance: ハイライト取得時のパフォーマンスを大幅に向上
- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上
## 2023.9.3
### General

View File

@@ -1248,8 +1248,6 @@ _sfx:
note: "الملاحظات"
noteMy: "ملاحظتي"
notification: "الإشعارات"
chat: "المحادثة"
chatBg: "المحادثة (الخلفية)"
antenna: "الهوائيات"
channel: "إشعارات القنات"
_ago:

View File

@@ -1020,8 +1020,6 @@ _sfx:
note: "নোটগুলি"
noteMy: "নোট (আপনার)"
notification: "বিজ্ঞপ্তি"
chat: "চ্যাট"
chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)"
antenna: "অ্যান্টেনাগুলি"
channel: "চ্যানেলের বিজ্ঞপ্তি"
_ago:

View File

@@ -398,7 +398,6 @@ _theme:
_sfx:
note: "Notes"
notification: "Notificacions"
chat: "Xat"
antenna: "Antenes"
_2fa:
renewTOTPCancel: "No, gràcies"

View File

@@ -1647,8 +1647,6 @@ _sfx:
note: "Poznámky"
noteMy: "Moje poznámka"
notification: "Oznámení"
chat: "Zprávy"
chatBg: "Chat (Pozadí)"
antenna: "Antény"
channel: "Oznámení kanálu"
_ago:

View File

@@ -1697,8 +1697,6 @@ _sfx:
note: "Notizen"
noteMy: "Meine Notizen"
notification: "Benachrichtigungen"
chat: "Chat"
chatBg: "Chat (Hintergrund)"
antenna: "Antennen"
channel: "Kanalbenachrichtigung"
_ago:

View File

@@ -303,8 +303,6 @@ _theme:
_sfx:
note: "Σημειώματα"
notification: "Ειδοποιήσεις"
chat: "Συνομιλία"
chatBg: "Συνομιλία (Παρασκήνιο)"
antenna: "Αντένες"
channel: "Ειδοποιήσεις καναλιών"
_ago:

View File

@@ -1697,8 +1697,6 @@ _sfx:
note: "New note"
noteMy: "Own note"
notification: "Notifications"
chat: "Chat"
chatBg: "Chat (Background)"
antenna: "Antennas"
channel: "Channel notifications"
_ago:

View File

@@ -1691,8 +1691,6 @@ _sfx:
note: "Notas"
noteMy: "Nota (a mí mismo)"
notification: "Notificaciones"
chat: "Chat"
chatBg: "Chat (Fondo)"
antenna: "Antena receptora"
channel: "Notificaciones del canal"
_ago:

View File

@@ -1355,8 +1355,6 @@ _sfx:
note: "Nouvelle note"
noteMy: "Ma note"
notification: "Notifications"
chat: "Discuter"
chatBg: "Discussion (arrière-plan)"
antenna: "Réception de lantenne"
channel: "Notifications de canal"
_ago:

View File

@@ -1652,8 +1652,6 @@ _sfx:
note: "Catatan"
noteMy: "Catatan (Saya)"
notification: "Notifikasi"
chat: "Pesan"
chatBg: "Obrolan (Latar Belakang)"
antenna: "Penerimaan Antenna"
channel: "Notifikasi Kanal"
_ago:

2
locales/index.d.ts vendored
View File

@@ -1808,8 +1808,6 @@ export interface Locale {
"note": string;
"noteMy": string;
"notification": string;
"chat": string;
"chatBg": string;
"antenna": string;
"channel": string;
};

View File

@@ -1692,8 +1692,6 @@ _sfx:
note: "Nota"
noteMy: "Mia nota"
notification: "Notifiche"
chat: "Messaggi"
chatBg: "Chat (sfondo)"
antenna: "Ricezione dell'antenna"
channel: "Notifiche di canale"
_ago:

View File

@@ -1725,8 +1725,6 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"

View File

@@ -1674,8 +1674,6 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"
_ago:

View File

@@ -1688,8 +1688,6 @@ _sfx:
note: "새 노트"
noteMy: "내 노트"
notification: "알림"
chat: "대화"
chatBg: "대화 (백그라운드)"
antenna: "안테나 수신"
channel: "채널 알림"
_ago:

View File

@@ -407,7 +407,6 @@ _theme:
_sfx:
note: "ບັນທຶກ"
notification: "ການແຈ້ງເຕືອນ"
chat: "ແຊ໋ດ"
_2fa:
renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
_widgets:

View File

@@ -438,7 +438,6 @@ _theme:
_sfx:
note: "Notities"
notification: "Meldingen"
chat: "Chat"
_2fa:
renewTOTPCancel: "Nee, bedankt"
_widgets:

View File

@@ -1066,8 +1066,6 @@ _sfx:
note: "Wpisy"
noteMy: "Mój wpis"
notification: "Powiadomienia"
chat: "Wiadomości"
chatBg: "Rozmowy (tło)"
antenna: "Anteny"
channel: "Powiadomienia kanału"
_ago:

View File

@@ -1320,7 +1320,6 @@ _theme:
_sfx:
note: "Posts"
notification: "Notificações"
chat: "Chat"
_ago:
invalid: "Não há nada aqui"
_timelineTutorial:

View File

@@ -647,7 +647,6 @@ _theme:
_sfx:
note: "Note"
notification: "Notificări"
chat: "Chat"
_ago:
invalid: "Nu e nimic de văzut aici"
_widgets:

View File

@@ -1576,8 +1576,6 @@ _sfx:
note: "Заметки"
noteMy: "Собственные заметки"
notification: "Уведомления"
chat: "Сообщения"
chatBg: "Сообщения (фон)"
antenna: "Антенна"
channel: "Канал"
_ago:

View File

@@ -1127,8 +1127,6 @@ _sfx:
note: "Poznámky"
noteMy: "Vlastná poznámka"
notification: "Oznámenia"
chat: "Chat"
chatBg: "Chat (pozadie)"
antenna: "Antény"
channel: "Upozornenia kanála"
_ago:

View File

@@ -507,7 +507,6 @@ _theme:
_sfx:
note: "Noter"
notification: "Notifikationer"
chat: "Chatt"
antenna: "Antenner"
_2fa:
renewTOTPCancel: "Nej tack"

View File

@@ -1686,8 +1686,6 @@ _sfx:
note: "หมายเหตุ"
noteMy: "โน้ตของตัวเอง"
notification: "การเเจ้งเตือน"
chat: "แชท"
chatBg: "แชท (พื้นหลัง)"
antenna: "เสาอากาศ"
channel: "การแจ้งเตือนช่อง"
_ago:

View File

@@ -386,7 +386,6 @@ _theme:
_sfx:
note: "notlar"
notification: "Bildirim"
chat: "Mesajlar"
_2fa:
renewTOTPCancel: "Hayır, teşekkürler"
_permissions:

View File

@@ -1315,8 +1315,6 @@ _sfx:
note: "Нотатки"
noteMy: "Мої нотатки"
notification: "Сповіщення"
chat: "Чати"
chatBg: "Чати (фон)"
antenna: "Прийом антени"
channel: "Повідомлення каналу"
_ago:

View File

@@ -910,7 +910,6 @@ _theme:
_sfx:
note: "Qaydlar"
notification: "Xabarnomalar"
chat: "Suhbat"
_ago:
minutesAgo: "{n} daqiqa oldin"
hoursAgo: "{n} soat oldin"

View File

@@ -1492,8 +1492,6 @@ _sfx:
note: "Tút"
noteMy: "Tút của tôi"
notification: "Thông báo"
chat: "Trò chuyện"
chatBg: "Chat (Nền)"
antenna: "Trạm phát sóng"
channel: "Kênh"
_ago:

View File

@@ -1697,8 +1697,6 @@ _sfx:
note: "帖子"
noteMy: "我的帖子"
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天线接收"
channel: "频道通知"
_ago:

View File

@@ -1695,8 +1695,6 @@ _sfx:
note: "貼文"
noteMy: "我的貼文"
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天線接收"
channel: "頻道通知"
_ago:

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.10.0-beta.2",
"version": "2023.10.0-beta.4",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -47,12 +47,12 @@
"cssnano": "6.0.1",
"js-yaml": "4.1.0",
"postcss": "8.4.31",
"terser": "5.20.0",
"terser": "5.21.0",
"typescript": "5.2.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"cross-env": "7.0.3",
"cypress": "13.3.0",
"eslint": "8.50.0",

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696405744672 {
name = 'CleanUp1696405744672'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `);
await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `);
}
}

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696569742153 {
name = 'CleanUp1696569742153'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`);
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`);
await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
}
}

View File

@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CleanUp1696581429196 {
name = 'CleanUp1696581429196'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`);
}
async down(queryRunner) {
}
}

View File

@@ -71,14 +71,14 @@
"@fastify/multipart": "8.0.0",
"@fastify/static": "6.11.2",
"@fastify/view": "8.2.0",
"@nestjs/common": "10.2.6",
"@nestjs/core": "10.2.6",
"@nestjs/testing": "10.2.6",
"@nestjs/common": "10.2.7",
"@nestjs/core": "10.2.7",
"@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.2.0",
"@sinonjs/fake-timers": "11.1.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.90",
"@swc/core": "1.3.92",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "6.0.1",
@@ -86,7 +86,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.11.4",
"bullmq": "4.12.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@@ -155,7 +155,7 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.9",
"systeminformation": "5.21.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
@@ -189,7 +189,7 @@
"@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.2",
"@types/ms": "0.7.32",
"@types/node": "20.7.1",
"@types/node": "20.8.2",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2",
@@ -212,8 +212,8 @@
"@types/vary": "1.1.1",
"@types/web-push": "3.6.1",
"@types/ws": "8.5.6",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.50.0",

View File

@@ -158,9 +158,13 @@ export class AnnouncementService {
if (moderator) {
if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
});
} else {
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {

View File

@@ -60,6 +60,7 @@ import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js';
@@ -187,6 +188,7 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@@ -318,6 +320,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService,
SearchService,
ClipService,
FeaturedService,
ChartLoggerService,
FederationChart,
NotesChart,
@@ -442,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService,
$SearchService,
$ClipService,
$FeaturedService,
$ChartLoggerService,
$FederationChart,
$NotesChart,
@@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService,
SearchService,
ClipService,
FeaturedService,
FederationChart,
NotesChart,
UsersChart,
@@ -690,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService,
$SearchService,
$ClipService,
$FeaturedService,
$FederationChart,
$NotesChart,
$UsersChart,

View File

@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiNote } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
@Injectable()
export class FeaturedService {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
) {
}
@bindThis
private getCurrentWindow(windowRange: number): number {
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
return Math.floor(passed / windowRange);
}
@bindThis
private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const redisTransaction = this.redisClient.multi();
redisTransaction.zincrby(
`${name}:${currentWindow}`,
score,
element);
redisTransaction.expire(
`${name}:${currentWindow}`,
(windowRange * 3) / 1000,
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
await redisTransaction.exec();
}
@bindThis
private async getRankingOf(name: string, windowRange: number, limit: number): Promise<string[]> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const [currentRankingResult, previousRankingResult] = await Promise.all([
this.redisClient.zrange(
`${name}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
this.redisClient.zrange(
`${name}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
]);
const ranking = new Map<string, number>();
for (let i = 0; i < currentRankingResult.length; i += 2) {
const noteId = currentRankingResult[i];
const score = parseInt(currentRankingResult[i + 1], 10);
ranking.set(noteId, score);
}
for (let i = 0; i < previousRankingResult.length; i += 2) {
const noteId = previousRankingResult[i];
const score = parseInt(previousRankingResult[i + 1], 10);
const exist = ranking.get(noteId);
if (exist != null) {
ranking.set(noteId, (exist + score) / 2);
} else {
ranking.set(noteId, score);
}
}
return Array.from(ranking.keys());
}
@bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public updateInChannelNotesRanking(noteId: MiNote['id'], channelId: MiNote['channelId'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit);
}
@bindThis
public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit);
}
}

View File

@@ -53,6 +53,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -200,6 +201,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private hashtagService: HashtagService,
private antennaService: AntennaService,
private webhookService: WebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
@@ -252,19 +254,30 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
throw new Error('Renote target is not public or home');
}
if (data.renote) {
switch (data.renote.visibility) {
case 'public':
// public noteは無条件にrenote可能
break;
case 'home':
// home noteはhome以下にrenote可能
if (data.visibility === 'public') {
data.visibility = 'home';
}
break;
case 'followers':
// 他人のfollowers noteはreject
if (data.renote.userId !== user.id) {
throw new Error('Renote target is not public or home');
}
// Renote対象がpublicではないならhomeにする
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// Renote対象がfollowersならfollowersにする
if (data.renote && data.renote.visibility === 'followers') {
data.visibility = 'followers';
// Renote対象がfollowersならfollowersにする
data.visibility = 'followers';
break;
case 'specified':
// specified / direct noteはreject
throw new Error('Renote target is not public or home');
}
}
// 返信対象がpublicではないならhomeにする
@@ -508,9 +521,8 @@ export class NoteCreateService implements OnApplicationShutdown {
});
}
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
if (!user.isBot) this.incRenoteCount(data.renote);
if (data.renote && data.renote.userId !== user.id && !user.isBot) {
this.incRenoteCount(data.renote);
}
if (data.poll && data.poll.expiresAt) {
@@ -710,10 +722,18 @@ export class NoteCreateService implements OnApplicationShutdown {
this.notesRepository.createQueryBuilder().update()
.set({
renoteCount: () => '"renoteCount" + 1',
score: () => '"score" + 1',
})
.where('id = :id', { id: renote.id })
.execute();
// 30%の確率でハイライト用ランキング更新
if (Math.random() < 0.3) {
if (renote.channelId != null) {
this.featuredService.updateInChannelNotesRanking(renote.id, renote.channelId, 1);
} else if (renote.visibility === 'public' && renote.userHost == null) {
this.featuredService.updateGlobalNotesRanking(renote.id, 1);
}
}
}
@bindThis
@@ -806,6 +826,12 @@ export class NoteCreateService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline();
if (note.channelId) {
redisPipeline.xadd(
`userTimelineWithChannel:${user.id}`,
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
'*',
'note', note.id);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
followeeId: note.channelId,

View File

@@ -64,12 +64,6 @@ export class NoteDeleteService {
const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1);
if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1);
}
if (note.replyId) {
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
}

View File

@@ -4,6 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤';
@@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
@Injectable()
export class ReactionService {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -86,6 +91,7 @@ export class ReactionService {
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -182,11 +188,19 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
... (!user.isBot ? { score: () => '"score" + 1' } : {}),
})
.where('id = :id', { id: note.id })
.execute();
// 30%の確率でハイライト用ランキング更新
if (Math.random() < 0.3 && note.userId !== user.id) {
if (note.channelId != null) {
this.featuredService.updateInChannelNotesRanking(note.id, note.channelId, 1);
} else if (note.visibility === 'public' && note.userHost == null) {
this.featuredService.updateGlobalNotesRanking(note.id, 1);
}
}
const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
@@ -275,8 +289,6 @@ export class ReactionService {
.where('id = :id', { id: note.id })
.execute();
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id,

View File

@@ -450,19 +450,4 @@ export class NoteEntityService implements OnModuleInit {
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
@bindThis
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
const query = this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId });
// 指定した投稿を除く
if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
}
return await query.getCount();
}
}

View File

@@ -146,64 +146,76 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
const following = await this.followingsRepository.findOneBy({
followerId: me,
followeeId: target,
});
return awaitAll({
id: target,
const [
following,
isFollowing: following != null,
isFollowed: this.followingsRepository.count({
isFollowed,
hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou,
isBlocking,
isBlocked,
isMuted,
isRenoteMuted,
] = await Promise.all([
this.followingsRepository.findOneBy({
followerId: me,
followeeId: target,
}),
this.followingsRepository.exist({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestFromYou: this.followRequestsRepository.count({
}),
this.followRequestsRepository.exist({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestToYou: this.followRequestsRepository.count({
}),
this.followRequestsRepository.exist({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
isBlocking: this.blockingsRepository.count({
}),
this.blockingsRepository.exist({
where: {
blockerId: me,
blockeeId: target,
},
take: 1,
}).then(n => n > 0),
isBlocked: this.blockingsRepository.count({
}),
this.blockingsRepository.exist({
where: {
blockerId: target,
blockeeId: me,
},
take: 1,
}).then(n => n > 0),
isMuted: this.mutingsRepository.count({
}),
this.mutingsRepository.exist({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
isRenoteMuted: this.renoteMutingsRepository.count({
}),
this.renoteMutingsRepository.exist({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
});
}),
]);
return {
id: target,
following,
isFollowing: following != null,
isFollowed,
hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou,
isBlocking,
isBlocked,
isMuted,
isRenoteMuted,
};
}
@bindThis
@@ -290,24 +302,6 @@ export class UserEntityService implements OnModuleInit {
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
// migration
if (user.avatarId != null && user.avatarUrl === null) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
this.usersRepository.update(user.id, {
avatarUrl: user.avatarUrl,
avatarBlurhash: avatar.blurhash,
});
}
if (user.bannerId != null && user.bannerUrl === null) {
const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
this.usersRepository.update(user.id, {
bannerUrl: user.bannerUrl,
bannerBlurhash: banner.blurhash,
});
}
const meId = me ? me.id : null;
const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;

View File

@@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isUserRelated(note: any, userIds: Set<string>): boolean {
if (userIds.has(note.userId)) {
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (userIds.has(note.userId) && !ignoreAuthor) {
return true;
}
if (note.reply != null && userIds.has(note.reply.userId)) {
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && userIds.has(note.renote.userId)) {
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
return true;
}

View File

@@ -18,7 +18,6 @@ export class MiNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Note.',
})
@@ -139,11 +138,6 @@ export class MiNote {
})
public url: string | null;
@Column('integer', {
default: 0, select: false,
})
public score: number;
@Index()
@Column({
...id(),
@@ -151,7 +145,6 @@ export class MiNote {
})
public fileIds: MiDriveFile['id'][];
@Index()
@Column('varchar', {
length: 256, array: true, default: '{}',
})

View File

@@ -14,7 +14,6 @@ export class MiNoteReaction {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the NoteReaction.',
})

View File

@@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isModerator: isModerator,
isSilenced: isSilenced,
isSuspended: user.isSuspended,
isHibernated: user.isHibernated,
lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote ?? '',
signins,

View File

@@ -79,14 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisForTimelines.xrevrange(
`channelTimeline:${channel.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
@@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timeline = await query.limit(ps.limit).getMany();
} else {
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@@ -57,6 +57,12 @@ export const meta = {
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
},
cannotRenoteDueToVisibility: {
message: 'You can not Renote due to target visibility.',
code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
},
noSuchReplyTarget: {
message: 'No such reply target.',
code: 'NO_SUCH_REPLY_TARGET',
@@ -231,6 +237,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
if (renote.visibility === 'followers' && renote.userId !== me.id) {
// 他人のfollowers noteはreject
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (renote.visibility === 'specified') {
// specified / direct noteはreject
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
}
}
let reply: MiNote | null = null;

View File

@@ -6,9 +6,9 @@
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { FeaturedService } from '@/core/FeaturedService.js';
export const meta = {
tags: ['notes'],
@@ -40,41 +40,50 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
private globalNotesRankingCache: string[] = [];
private globalNotesRankingCacheLastFetchedAt = 0;
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private featuredService: FeaturedService,
) {
super(meta, paramDef, async (ps, me) => {
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
let noteIds: string[];
if (ps.channelId) {
noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50);
} else {
if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
noteIds = this.globalNotesRankingCache;
} else {
noteIds = await this.featuredService.getGlobalNotesRanking(100);
this.globalNotesRankingCache = noteIds;
this.globalNotesRankingCacheLastFetchedAt = Date.now();
}
}
if (noteIds.length === 0) {
return [];
}
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds.slice(ps.offset, ps.offset + ps.limit);
const query = this.notesRepository.createQueryBuilder('note')
.addSelect('note.score')
.where('note.userHost IS NULL')
.andWhere('note.score > 0')
.andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
.andWhere('note.visibility = \'public\'')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
let notes = await query
.orderBy('note.score', 'DESC')
.limit(100)
.getMany();
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
notes = notes.slice(ps.offset, ps.offset + ps.limit);
// TODO: ミュート等考慮
return await this.noteEntityService.packMany(notes, me);
});

View File

@@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let htlNoteIdsRes: [string, string[]][] = [];
let ltlNoteIdsRes: [string, string[]][] = [];
@@ -100,18 +100,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.redisForTimelines.xrevrange(
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit),
this.redisForTimelines.xrevrange(
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'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);
const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);

View File

@@ -87,18 +87,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 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 ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@@ -78,18 +78,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
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 ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@@ -102,18 +102,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 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 ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.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';
export const meta = {
@@ -43,6 +44,7 @@ export const paramDef = {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withChannelNotes: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@@ -70,32 +72,47 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const [
userIdsWhoMeMuting,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
]) : [new Set<string>()];
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
let repliesNoteIdsRes: [string, string[]][] = [];
let channelNoteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
[noteIdsRes, repliesNoteIdsRes] = await Promise.all([
[noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = 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 ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit),
ps.withReplies
? this.redisForTimelines.xrevrange(
`userTimelineWithReplies:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit)
: Promise.resolve([]),
ps.withChannelNotes
? this.redisForTimelines.xrevrange(
`userTimelineWithChannel:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'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),
...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
...channelNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
]));
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
@@ -118,6 +135,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timeline = await query.getMany();
timeline = timeline.filter(note => {
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (ps.withRenotes === false) return false;

View File

@@ -171,6 +171,9 @@ export type ModerationLogPayloads = {
deleteUserAnnouncement: {
announcementId: string;
announcement: any;
userId: string;
userUsername: string;
userHost: string | null;
};
resetPassword: {
userId: string;

View File

@@ -15,6 +15,10 @@ function genHost() {
return randomString() + '.example.com';
}
function waitForPushToTl() {
return sleep(300);
}
let app: INestApplicationContext;
beforeAll(async () => {
@@ -32,7 +36,7 @@ describe('Timelines', () => {
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -44,10 +48,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -59,10 +64,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
const carolNote = await post(carol, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -75,10 +81,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -91,10 +98,11 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice);
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -107,10 +115,11 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice);
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
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に追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -123,10 +132,11 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice);
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -140,10 +150,11 @@ describe('Timelines', () => {
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);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -158,10 +169,11 @@ describe('Timelines', () => {
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);
await sleep(1000);
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に追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -173,10 +185,11 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -190,7 +203,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi' });
const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -202,10 +215,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -217,10 +231,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {
withRenotes: false,
@@ -234,10 +249,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {
withRenotes: false,
@@ -251,9 +267,10 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -265,10 +282,11 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice);
await api('/mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -282,10 +300,11 @@ describe('Timelines', () => {
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);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -297,9 +316,10 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -310,9 +330,10 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
@@ -323,6 +344,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
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'),
@@ -332,7 +354,7 @@ describe('Timelines', () => {
const carolNote1 = await post(carol, { text: 'hi' });
const carolNote2 = await post(carol, { fileIds: [carolFile.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/timeline', { withFiles: true }, alice);
@@ -341,6 +363,21 @@ describe('Timelines', () => {
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);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
});
describe('Local TL', () => {
@@ -350,7 +387,7 @@ describe('Timelines', () => {
const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -358,12 +395,25 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
});
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
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() })]);
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -374,13 +424,12 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/following/create', {
userId: carol.id,
}, alice);
await api('/following/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -392,10 +441,11 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -408,10 +458,11 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice);
await api('/mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -425,10 +476,11 @@ describe('Timelines', () => {
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);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -443,7 +495,7 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', { withFiles: true }, alice);
@@ -458,7 +510,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', {}, alice);
@@ -470,7 +522,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', {}, alice);
@@ -481,9 +533,10 @@ describe('Timelines', () => {
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: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', {}, alice);
@@ -495,7 +548,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
@@ -506,9 +559,10 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', {}, alice);
@@ -519,9 +573,10 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', {}, alice);
@@ -535,7 +590,7 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/hybrid-timeline', { withFiles: true }, alice);
@@ -550,9 +605,10 @@ describe('Timelines', () => {
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 sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -564,9 +620,10 @@ describe('Timelines', () => {
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 sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -579,9 +636,10 @@ describe('Timelines', () => {
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 sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -594,9 +652,10 @@ describe('Timelines', () => {
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 sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -609,10 +668,11 @@ describe('Timelines', () => {
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 sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -624,10 +684,11 @@ describe('Timelines', () => {
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 sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -641,10 +702,11 @@ describe('Timelines', () => {
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);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -657,9 +719,10 @@ describe('Timelines', () => {
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);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -672,9 +735,10 @@ describe('Timelines', () => {
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);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
@@ -682,6 +746,22 @@ describe('Timelines', () => {
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
});
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
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 sleep(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
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('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
@@ -691,7 +771,7 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
@@ -706,7 +786,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
@@ -718,7 +798,7 @@ describe('Timelines', () => {
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
@@ -732,7 +812,7 @@ describe('Timelines', () => {
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
@@ -740,6 +820,19 @@ describe('Timelines', () => {
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
});
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
@@ -747,7 +840,7 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
@@ -762,7 +855,7 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
@@ -770,6 +863,21 @@ describe('Timelines', () => {
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 waitForPushToTl();
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()]);
@@ -777,13 +885,59 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await sleep(100); // redisに追加されるのを待つ
await waitForPushToTl();
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('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('/mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
await api('/mute/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
await waitForPushToTl();
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), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true);
});
});
// TODO: リノートミュート済みユーザーのテスト

View File

@@ -92,6 +92,9 @@ describe('ActivityPub', () => {
const metaInitial = {
cacheRemoteFiles: true,
cacheRemoteSensitiveFiles: true,
perUserHomeTimelineCacheMax: 100,
perLocalUserUserTimelineCacheMax: 100,
perRemoteUserUserTimelineCacheMax: 100,
blockedHosts: [] as string[],
sensitiveWords: [] as string[],
} as MiMeta;

View File

@@ -18,13 +18,13 @@
"dependencies": {
"@discordapp/twemoji": "14.1.2",
"@github/webauthn-json": "2.1.1",
"@rollup/plugin-alias": "5.0.0",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.4",
"@rollup/plugin-alias": "5.0.1",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-replace": "5.0.3",
"@rollup/pluginutils": "5.0.5",
"@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.37.0",
"@vitejs/plugin-vue": "4.3.4",
"@vitejs/plugin-vue": "4.4.0",
"@vue-macros/reactivity-transform": "0.3.23",
"@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6",
@@ -38,7 +38,7 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "7.2.0",
"chromatic": "7.2.2",
"compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0",
@@ -53,13 +53,13 @@
"matter-js": "0.19.0",
"mfm-js": "0.23.3",
"misskey-js": "workspace:*",
"photoswipe": "5.4.1",
"photoswipe": "5.4.2",
"prismjs": "1.29.0",
"punycode": "2.3.0",
"querystring": "0.2.1",
"rollup": "3.29.4",
"rollup": "4.0.0",
"sanitize-html": "2.11.0",
"sass": "1.68.0",
"sass": "1.69.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.157.0",
@@ -72,36 +72,36 @@
"uuid": "9.0.1",
"v-code-diff": "1.7.1",
"vanilla-tilt": "1.8.1",
"vite": "4.4.9",
"vite": "4.4.11",
"vue": "3.3.4",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.4.5",
"@storybook/addon-essentials": "7.4.5",
"@storybook/addon-interactions": "7.4.5",
"@storybook/addon-links": "7.4.5",
"@storybook/addon-storysource": "7.4.5",
"@storybook/addons": "7.4.5",
"@storybook/blocks": "7.4.5",
"@storybook/core-events": "7.4.5",
"@storybook/jest": "0.2.2",
"@storybook/manager-api": "7.4.5",
"@storybook/preview-api": "7.4.5",
"@storybook/react": "7.4.5",
"@storybook/react-vite": "7.4.5",
"@storybook/testing-library": "0.2.1",
"@storybook/theming": "7.4.5",
"@storybook/types": "7.4.5",
"@storybook/vue3": "7.4.5",
"@storybook/vue3-vite": "7.4.5",
"@storybook/addon-actions": "7.4.6",
"@storybook/addon-essentials": "7.4.6",
"@storybook/addon-interactions": "7.4.6",
"@storybook/addon-links": "7.4.6",
"@storybook/addon-storysource": "7.4.6",
"@storybook/addons": "7.4.6",
"@storybook/blocks": "7.4.6",
"@storybook/core-events": "7.4.6",
"@storybook/jest": "0.2.3",
"@storybook/manager-api": "7.4.6",
"@storybook/preview-api": "7.4.6",
"@storybook/react": "7.4.6",
"@storybook/react-vite": "7.4.6",
"@storybook/testing-library": "0.2.2",
"@storybook/theming": "7.4.6",
"@storybook/types": "7.4.6",
"@storybook/vue3": "7.4.6",
"@storybook/vue3-vite": "7.4.6",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.2",
"@types/matter-js": "0.19.1",
"@types/micromatch": "4.0.3",
"@types/node": "20.7.1",
"@types/node": "20.8.2",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.1",
"@types/throttle-debounce": "5.0.0",
@@ -109,9 +109,9 @@
"@types/uuid": "9.0.4",
"@types/websocket": "1.0.7",
"@types/ws": "8.5.6",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@vitest/coverage-v8": "0.34.5",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.3.4",
"acorn": "8.10.0",
"cross-env": "7.0.3",
@@ -122,18 +122,18 @@
"fast-glob": "3.3.1",
"happy-dom": "10.0.3",
"micromatch": "4.0.5",
"msw": "1.3.1",
"msw": "1.3.2",
"msw-storybook-addon": "1.8.0",
"nodemon": "3.0.1",
"prettier": "3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.1",
"storybook": "7.4.5",
"storybook": "7.4.6",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.5",
"vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.15"

View File

@@ -215,7 +215,7 @@ const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
const translation = ref<any>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
const keymap = {

View File

@@ -68,8 +68,8 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.instanceBlocking,
icon: 'ti ti-ban',
title: i18n.ts.externalServices,
icon: 'ti ti-link',
});
</script>

View File

@@ -29,8 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
<span v-else-if="log.type === 'createUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'updateUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
</template>
@@ -88,6 +92,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateGlobalAnnouncement'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateUserAnnouncement'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<details>
<summary>raw</summary>

View File

@@ -139,21 +139,11 @@ const menuDef = computed(() => [{
text: i18n.ts.roles,
to: '/settings/roles',
active: currentPage?.route.name === 'roles',
}, {
icon: 'ti ti-planet-off',
text: i18n.ts.instanceMute,
to: '/settings/instance-mute',
active: currentPage?.route.name === 'instance-mute',
}, {
icon: 'ti ti-ban',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
active: currentPage?.route.name === 'mute-block',
}, {
icon: 'ti ti-message-off',
text: i18n.ts.wordMute,
to: '/settings/word-mute',
active: currentPage?.route.name === 'word-mute',
}, {
icon: 'ti ti-api',
text: 'API',

View File

@@ -22,7 +22,6 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const instanceMutes = ref($i!.mutedInstances.join('\n'));
const changed = ref(false);
@@ -46,13 +45,4 @@ async function save() {
watch(instanceMutes, () => {
changed.value = true;
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.instanceMute,
icon: 'ti ti-planet-off',
});
</script>

View File

@@ -5,6 +5,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
<XWordMute/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-planet-off"></i></template>
<template #label>{{ i18n.ts.instanceMute }}</template>
<XInstanceMute/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-repeat-off"></i></template>
<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
@@ -106,6 +120,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';

View File

@@ -91,13 +91,4 @@ async function save() {
changed.value = false;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.wordMute,
icon: 'ti ti-message-off',
});
</script>

View File

@@ -38,14 +38,12 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
note: soundConfigStore.reactiveState.sound_note,
noteMy: soundConfigStore.reactiveState.sound_noteMy,
notification: soundConfigStore.reactiveState.sound_notification,
chat: soundConfigStore.reactiveState.sound_chat,
chatBg: soundConfigStore.reactiveState.sound_chatBg,
antenna: soundConfigStore.reactiveState.sound_antenna,
channel: soundConfigStore.reactiveState.sound_channel,
});

View File

@@ -29,7 +29,7 @@ const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
const include = ref<string | null>(null);
const include = ref<string | null>('all');
const pagination = {
endpoint: 'users/notes' as const,
@@ -38,6 +38,7 @@ const pagination = {
userId: props.user.id,
withRenotes: include.value === 'all',
withReplies: include.value === 'all' || include.value === 'files',
withChannelNotes: include.value === 'all',
withFiles: include.value === 'files',
})),
};

View File

@@ -126,18 +126,10 @@ export const routes = [{
path: '/import-export',
name: 'import-export',
component: page(() => import('./pages/settings/import-export.vue')),
}, {
path: '/instance-mute',
name: 'instance-mute',
component: page(() => import('./pages/settings/instance-mute.vue')),
}, {
path: '/mute-block',
name: 'mute-block',
component: page(() => import('./pages/settings/mute-block.vue')),
}, {
path: '/word-mute',
name: 'word-mute',
component: page(() => import('./pages/settings/word-mute.vue')),
}, {
path: '/api',
name: 'api',

View File

@@ -27,14 +27,6 @@ export const soundConfigStore = markRaw(new Storage('sound', {
where: 'account',
default: { type: 'syuilo/n-ea', volume: 1 },
},
sound_chat: {
where: 'account',
default: { type: 'syuilo/pope1', volume: 1 },
},
sound_chatBg: {
where: 'account',
default: { type: 'syuilo/waon', volume: 1 },
},
sound_antenna: {
where: 'account',
default: { type: 'syuilo/triple', volume: 1 },

View File

@@ -20,12 +20,12 @@
"url": "git+https://github.com/misskey-dev/misskey.js.git"
},
"devDependencies": {
"@microsoft/api-extractor": "7.37.2",
"@microsoft/api-extractor": "7.38.0",
"@swc/jest": "0.2.29",
"@types/jest": "29.5.5",
"@types/node": "20.7.1",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@types/node": "20.8.2",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"eslint": "8.50.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
@@ -39,7 +39,7 @@
],
"dependencies": {
"@swc/cli": "0.1.62",
"@swc/core": "1.3.90",
"@swc/core": "1.3.92",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View File

@@ -189,6 +189,9 @@ export type ModerationLogPayloads = {
deleteUserAnnouncement: {
announcementId: string;
announcement: any;
userId: string;
userUsername: string;
userHost: string | null;
};
resetPassword: {
userId: string;

View File

@@ -14,7 +14,7 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "6.7.3",
"@typescript-eslint/parser": "6.7.4",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.50.0",
"eslint-plugin-import": "2.28.1",

1532
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff