Compare commits
87 Commits
a92835d4e3
...
a029d5b68b
Author | SHA1 | Date |
---|---|---|
|
a029d5b68b | |
|
218070eb13 | |
|
0f8c068e84 | |
|
69d66b89f2 | |
|
211365de64 | |
|
966127c63e | |
|
54800971eb | |
|
13d5c6d2b2 | |
|
2cff00eedd | |
|
3fc2261041 | |
|
97f4c6a2e6 | |
|
d72119fd08 | |
|
fda1e7fe10 | |
|
98926c339e | |
|
c39842a07c | |
|
b07ca4652b | |
|
457b53d5ea | |
|
8a39b391ad | |
|
9c4b01d69f | |
|
6f83e9decb | |
|
be688922c8 | |
|
634f16f6b0 | |
|
03e404ce27 | |
|
449fadd15a | |
|
6aad9299dc | |
|
de97a3e04b | |
|
a3528182f0 | |
|
1ab29c2085 | |
|
2960c2069e | |
|
f18e44f6fa | |
|
761c19adbe | |
|
382d6567a5 | |
|
ef6e3ca2ad | |
|
f81cc413ad | |
|
2cc46fc331 | |
|
89d919e4a4 | |
|
1c03446304 | |
|
db67e81b99 | |
|
90c4551dfe | |
|
3dac916640 | |
|
9447bbee44 | |
|
d4065d5a2b | |
|
4bac864b07 | |
|
090ab19f28 | |
|
c7a5b92764 | |
|
002e69d8c8 | |
|
32bdaadfc0 | |
|
8850ea53ca | |
|
3e9d3421c2 | |
|
5a756c077d | |
|
306fe94092 | |
|
bbdfb421f5 | |
|
a6cdcfb2cf | |
|
a24c4951dd | |
|
1fc2ae025c | |
|
bb4af983b7 | |
|
f7f4cff66a | |
|
4e69755afa | |
|
3f4e80f5bf | |
|
25d2121aa7 | |
|
7476dda563 | |
|
cb38757e59 | |
|
eee0aba8af | |
|
537a49a448 | |
|
f50364b100 | |
|
399e527cc2 | |
|
90eccd4d95 | |
|
9961eca0b8 | |
|
5164a94e96 | |
|
85f4730308 | |
|
f5d4b2744b | |
|
7e648b57b7 | |
|
5828e6d9f8 | |
|
37b4340619 | |
|
26e6c148cb | |
|
c5928980f8 | |
|
4a4a57dea5 | |
|
16cb881fa2 | |
|
6ad3f07d48 | |
|
08e4fedf6c | |
|
479525a71d | |
|
4da98fe394 | |
|
cbc85eef3e | |
|
3a5870b9a5 | |
|
77a11e369f | |
|
e5a0d2775a | |
|
02ac7d0029 |
|
@ -105,6 +105,16 @@ port: 3000
|
|||
# socket: /path/to/misskey.sock
|
||||
# chmodSocket: '777'
|
||||
|
||||
# Proxy trust settings
|
||||
#
|
||||
# Changes how the server interpret the origin IP of the request.
|
||||
#
|
||||
# Any format supported by Fastify is accepted.
|
||||
# Default: trust all proxies (i.e. trustProxy: true)
|
||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
||||
#
|
||||
# trustProxy: 1
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
## 2025.9.1
|
||||
|
||||
### NOTE
|
||||
- pnpm 10.16.0 が必要です
|
||||
|
||||
### General
|
||||
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
|
||||
|
||||
### Client
|
||||
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
|
||||
- Feat: 動画を圧縮してアップロードできるようになりました
|
||||
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
||||
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加
|
||||
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
|
||||
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
|
||||
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
||||
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
|
||||
|
||||
## 2025.9.0
|
||||
|
||||
|
|
|
@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
|
|||
* 処理中
|
||||
*/
|
||||
"processing": string;
|
||||
/**
|
||||
* 準備中
|
||||
*/
|
||||
"preprocessing": string;
|
||||
/**
|
||||
* プレビュー
|
||||
*/
|
||||
|
@ -5509,6 +5513,14 @@ export interface Locale extends ILocale {
|
|||
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
||||
*/
|
||||
"defaultImageCompressionLevel_description": string;
|
||||
/**
|
||||
* デフォルトの圧縮度
|
||||
*/
|
||||
"defaultCompressionLevel": string;
|
||||
/**
|
||||
* 低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。
|
||||
*/
|
||||
"defaultCompressionLevel_description": string;
|
||||
/**
|
||||
* 分
|
||||
*/
|
||||
|
@ -5541,6 +5553,36 @@ export interface Locale extends ILocale {
|
|||
* ユーザー指定ノートを作成
|
||||
*/
|
||||
"createUserSpecifiedNote": string;
|
||||
"_compression": {
|
||||
"_quality": {
|
||||
/**
|
||||
* 高品質
|
||||
*/
|
||||
"high": string;
|
||||
/**
|
||||
* 中品質
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* 低品質
|
||||
*/
|
||||
"low": string;
|
||||
};
|
||||
"_size": {
|
||||
/**
|
||||
* サイズ大
|
||||
*/
|
||||
"large": string;
|
||||
/**
|
||||
* サイズ中
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* サイズ小
|
||||
*/
|
||||
"small": string;
|
||||
};
|
||||
};
|
||||
"_order": {
|
||||
/**
|
||||
* 新しい順
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
|
|||
pinLimitExceeded: "これ以上ピン留めできません"
|
||||
done: "完了"
|
||||
processing: "処理中"
|
||||
preprocessing: "準備中"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
defaultValueIs: "デフォルト: {value}"
|
||||
|
@ -1372,6 +1373,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
|||
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
||||
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
||||
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
||||
defaultCompressionLevel: "デフォルトの圧縮度"
|
||||
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
|
||||
inMinutes: "分"
|
||||
inDays: "日"
|
||||
safeModeEnabled: "セーフモードが有効です"
|
||||
|
@ -1381,6 +1384,16 @@ themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォル
|
|||
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
||||
createUserSpecifiedNote: "ユーザー指定ノートを作成"
|
||||
|
||||
_compression:
|
||||
_quality:
|
||||
high: "高品質"
|
||||
medium: "中品質"
|
||||
low: "低品質"
|
||||
_size:
|
||||
large: "サイズ大"
|
||||
medium: "サイズ中"
|
||||
small: "サイズ小"
|
||||
|
||||
_order:
|
||||
newest: "新しい順"
|
||||
oldest: "古い順"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.9.1-alpha.0",
|
||||
"version": "2025.9.1-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.1",
|
||||
"packageManager": "pnpm@10.16.0",
|
||||
"workspaces": [
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend",
|
||||
|
@ -76,7 +76,7 @@
|
|||
"eslint": "9.35.0",
|
||||
"globals": "16.3.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.15.1",
|
||||
"pnpm": "10.16.0",
|
||||
"start-server-and-test": "2.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowingIsFollowerSuspended1752410859370 {
|
||||
name = 'FollowingIsFollowerSuspended1752410859370'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerSuspended" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1896254b78a41a50e0396fdabd" ON "following" ("followeeId", "followerHost", "isFollowerSuspended", "isFollowerHibernated") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d2b8dbf0b772042f4fe241a29d" ON "following" ("followerId", "followeeId", "isFollowerSuspended") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d2b8dbf0b772042f4fe241a29d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_1896254b78a41a50e0396fdabd"`);
|
||||
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerSuspended"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowingIsFollowerSuspendedCopySuspendedState1752410900000 {
|
||||
name = 'FollowingIsFollowerSuspendedCopySuspendedState1752410900000'
|
||||
|
||||
async up(queryRunner) {
|
||||
// Update existing records based on user suspension status
|
||||
await queryRunner.query(`
|
||||
UPDATE "following"
|
||||
SET "isFollowerSuspended" = "user"."isSuspended"
|
||||
FROM "user"
|
||||
WHERE "following"."followerId" = "user"."id"
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import * as fs from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
@ -27,6 +28,7 @@ type Source = {
|
|||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
trustProxy?: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket?: string;
|
||||
disableHsts?: boolean;
|
||||
db: {
|
||||
|
@ -118,6 +120,7 @@ export type Config = {
|
|||
url: string;
|
||||
port: number;
|
||||
socket: string | undefined;
|
||||
trustProxy: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket: string | undefined;
|
||||
disableHsts: boolean | undefined;
|
||||
db: {
|
||||
|
@ -266,6 +269,7 @@ export function loadConfig(): Config {
|
|||
url: url.origin,
|
||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||
socket: config.socket,
|
||||
trustProxy: config.trustProxy,
|
||||
chmodSocket: config.chmodSocket,
|
||||
disableHsts: config.disableHsts,
|
||||
host,
|
||||
|
|
|
@ -549,6 +549,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
// TODO: キャッシュ
|
||||
this.followingsRepository.findBy({
|
||||
followeeId: user.id,
|
||||
isFollowerSuspended: false,
|
||||
notify: 'normal',
|
||||
}).then(async followings => {
|
||||
if (note.visibility !== 'specified') {
|
||||
|
@ -854,6 +855,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerHost: IsNull(),
|
||||
isFollowerSuspended: false,
|
||||
isFollowerHibernated: false,
|
||||
},
|
||||
select: ['followerId', 'withReplies'],
|
||||
|
|
|
@ -174,6 +174,9 @@ export class QueueService {
|
|||
@bindThis
|
||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||
if (content == null) return null;
|
||||
inboxes.delete(null as unknown as string); // remove null inboxes
|
||||
if (inboxes.size === 0) return null;
|
||||
|
||||
const contentBody = JSON.stringify(content);
|
||||
const digest = ApRequestCreator.createDigest(contentBody);
|
||||
|
||||
|
|
|
@ -229,9 +229,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followee: {
|
||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
||||
},
|
||||
follower: {
|
||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
||||
},
|
||||
follower: MiUser,
|
||||
silent = false,
|
||||
withReplies?: boolean,
|
||||
): Promise<void> {
|
||||
|
@ -244,6 +242,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
withReplies: withReplies,
|
||||
isFollowerSuspended: follower.isSuspended,
|
||||
|
||||
// 非正規化
|
||||
followerHost: follower.host,
|
||||
|
@ -734,6 +733,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
return this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: userId })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
|
@ -743,6 +743,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
where: {
|
||||
followerId,
|
||||
followeeId,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,13 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -29,9 +28,9 @@ export class UserSuspendService {
|
|||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
@ -49,8 +48,8 @@ export class UserSuspendService {
|
|||
});
|
||||
|
||||
(async () => {
|
||||
await this.postSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
await this.postSuspend(user).catch((e: any) => {});
|
||||
await this.suspendFollowings(user).catch((e: any) => {});
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -67,7 +66,8 @@ export class UserSuspendService {
|
|||
});
|
||||
|
||||
(async () => {
|
||||
await this.postUnsuspend(user).catch(e => {});
|
||||
await this.postUnsuspend(user).catch((e: any) => {});
|
||||
await this.restoreFollowings(user).catch((e: any) => {});
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -83,28 +83,11 @@ export class UserSuspendService {
|
|||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
{ followeeSharedInbox: Not(IsNull()) },
|
||||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user, content, inbox, true);
|
||||
}
|
||||
const manager = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||
manager.addAllKnowingSharedInboxRecipe();
|
||||
manager.addFollowersRecipe();
|
||||
manager.execute();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,50 +96,36 @@ export class UserSuspendService {
|
|||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにUndo Delete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
{ followeeSharedInbox: Not(IsNull()) },
|
||||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user as any, content, inbox, true);
|
||||
}
|
||||
const manager = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||
manager.addAllKnowingSharedInboxRecipe();
|
||||
manager.addFollowersRecipe();
|
||||
manager.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: MiUser) {
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
private async suspendFollowings(follower: MiUser) {
|
||||
await this.followingsRepository.update(
|
||||
{
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
{
|
||||
isFollowerSuspended: true,
|
||||
}
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async restoreFollowings(follower: MiUser) {
|
||||
// フォロー関係を復元(isFollowerSuspended: false)に変更
|
||||
await this.followingsRepository.update(
|
||||
{
|
||||
followerId: follower.id,
|
||||
},
|
||||
{
|
||||
isFollowerSuspended: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import { ThinUser } from '@/queue/types.js';
|
||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
|
||||
interface IRecipe {
|
||||
type: string;
|
||||
|
@ -27,12 +29,19 @@ interface IDirectRecipe extends IRecipe {
|
|||
to: MiRemoteUser;
|
||||
}
|
||||
|
||||
interface IAllKnowingSharedInboxRecipe extends IRecipe {
|
||||
type: 'AllKnowingSharedInbox';
|
||||
}
|
||||
|
||||
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||
recipe.type === 'Followers';
|
||||
|
||||
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||
recipe.type === 'Direct';
|
||||
|
||||
const isAllKnowingSharedInbox = (recipe: IRecipe): recipe is IAllKnowingSharedInboxRecipe =>
|
||||
recipe.type === 'AllKnowingSharedInbox';
|
||||
|
||||
class DeliverManager {
|
||||
private actor: ThinUser;
|
||||
private activity: IActivity | null;
|
||||
|
@ -40,16 +49,15 @@ class DeliverManager {
|
|||
|
||||
/**
|
||||
* Constructor
|
||||
* @param userEntityService
|
||||
* @param followingsRepository
|
||||
* @param queueService
|
||||
* @param actor Actor
|
||||
* @param activity Activity to deliver
|
||||
*/
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private queueService: QueueService,
|
||||
private logger: Logger,
|
||||
|
||||
actor: { id: MiUser['id']; host: null; },
|
||||
activity: IActivity | null,
|
||||
|
@ -91,6 +99,18 @@ class DeliverManager {
|
|||
this.addRecipe(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe for all-knowing shared inbox deliver
|
||||
*/
|
||||
@bindThis
|
||||
public addAllKnowingSharedInboxRecipe(): void {
|
||||
const deliver: IAllKnowingSharedInboxRecipe = {
|
||||
type: 'AllKnowingSharedInbox',
|
||||
};
|
||||
|
||||
this.addRecipe(deliver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe
|
||||
* @param recipe Recipe
|
||||
|
@ -104,11 +124,30 @@ class DeliverManager {
|
|||
* Execute delivers
|
||||
*/
|
||||
@bindThis
|
||||
public async execute(): Promise<void> {
|
||||
public async execute(opts: { ignoreSuspend?: boolean } = {}): Promise<void> {
|
||||
//#region collect inboxes by recipes
|
||||
// The value flags whether it is shared or not.
|
||||
// key: inbox URL, value: whether it is sharedInbox
|
||||
const inboxes = new Map<string, boolean>();
|
||||
|
||||
if (this.recipes.some(r => isAllKnowingSharedInbox(r))) {
|
||||
// all-knowing shared inbox
|
||||
const followings = await this.followingsRepository.createQueryBuilder('f')
|
||||
.select([
|
||||
'f.followerSharedInbox',
|
||||
'f.followeeSharedInbox',
|
||||
])
|
||||
.where('f.followerSharedInbox IS NOT NULL')
|
||||
.orWhere('f.followeeSharedInbox IS NOT NULL')
|
||||
.distinct()
|
||||
.getRawMany<{ f_followerSharedInbox: string | null; f_followeeSharedInbox: string | null; }>();
|
||||
|
||||
for (const following of followings) {
|
||||
if (following.f_followeeSharedInbox) inboxes.set(following.f_followeeSharedInbox, true);
|
||||
if (following.f_followerSharedInbox) inboxes.set(following.f_followerSharedInbox, true);
|
||||
}
|
||||
}
|
||||
|
||||
// build inbox list
|
||||
// Process follower recipes first to avoid duplication when processing direct recipes later.
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
|
@ -119,6 +158,7 @@ class DeliverManager {
|
|||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
isFollowerSuspended: opts.ignoreSuspend ? undefined : false,
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
|
@ -142,21 +182,26 @@ class DeliverManager {
|
|||
|
||||
inboxes.set(recipe.to.inbox, false);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// deliver
|
||||
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApDeliverManagerService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queueService: QueueService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger.createSubLogger('deliver-manager');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,9 +212,9 @@ export class ApDeliverManagerService {
|
|||
@bindThis
|
||||
public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
this.queueService,
|
||||
this.logger,
|
||||
actor,
|
||||
activity,
|
||||
);
|
||||
|
@ -186,9 +231,9 @@ export class ApDeliverManagerService {
|
|||
@bindThis
|
||||
public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise<void> {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
this.queueService,
|
||||
this.logger,
|
||||
actor,
|
||||
activity,
|
||||
);
|
||||
|
@ -205,9 +250,9 @@ export class ApDeliverManagerService {
|
|||
@bindThis
|
||||
public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise<void> {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
this.queueService,
|
||||
this.logger,
|
||||
actor,
|
||||
activity,
|
||||
);
|
||||
|
@ -218,10 +263,9 @@ export class ApDeliverManagerService {
|
|||
@bindThis
|
||||
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
|
||||
return new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
this.queueService,
|
||||
|
||||
this.logger,
|
||||
actor,
|
||||
activity,
|
||||
);
|
||||
|
|
|
@ -9,7 +9,8 @@ import { MiUser } from './User.js';
|
|||
|
||||
@Entity('following')
|
||||
@Index(['followerId', 'followeeId'], { unique: true })
|
||||
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||
@Index(['followerId', 'followeeId', 'isFollowerSuspended'])
|
||||
@Index(['followeeId', 'followerHost', 'isFollowerSuspended', 'isFollowerHibernated'])
|
||||
export class MiFollowing {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
@ -45,6 +46,11 @@ export class MiFollowing {
|
|||
})
|
||||
public isFollowerHibernated: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isFollowerSuspended: boolean;
|
||||
|
||||
// タイムラインにその人のリプライまで含めるかどうか
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
|
|
@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: true,
|
||||
trustProxy: this.config.trustProxy ?? true,
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
|
|
@ -50,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followeeHost = :host', { host: ps.host });
|
||||
.andWhere('following.followeeHost = :host', { host: ps.host })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -50,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followerHost = :host', { host: ps.host });
|
||||
.andWhere('following.followerHost = :host', { host: ps.host })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -94,11 +94,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.followingsRepository.count({
|
||||
where: {
|
||||
followeeHost: Not(IsNull()),
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
}),
|
||||
this.followingsRepository.count({
|
||||
where: {
|
||||
followerHost: Not(IsNull()),
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -125,6 +125,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
|
@ -136,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.innerJoinAndSelect('following.follower', 'follower');
|
||||
|
||||
const followings = await query
|
||||
|
|
|
@ -133,6 +133,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
|
@ -144,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
if (ps.birthday) {
|
||||
|
|
|
@ -68,7 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
.where('following.followerId = :followerId', { followerId: me.id })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
query
|
||||
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
|
||||
|
|
|
@ -0,0 +1,620 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
describe('ApDeliverManagerService', () => {
|
||||
let service: ApDeliverManagerService;
|
||||
let followingsRepository: jest.Mocked<FollowingsRepository>;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let apLoggerService: jest.Mocked<ApLoggerService>;
|
||||
|
||||
const mockLocalUser: MiLocalUser = {
|
||||
id: 'local-user-id',
|
||||
host: null,
|
||||
} as MiLocalUser;
|
||||
|
||||
const mockRemoteUser1: MiRemoteUser & { inbox: string; sharedInbox: string; } = {
|
||||
id: 'remote-user-1',
|
||||
host: 'remote.example.com',
|
||||
inbox: 'https://remote.example.com/inbox',
|
||||
sharedInbox: 'https://remote.example.com/shared-inbox',
|
||||
} as MiRemoteUser & { inbox: string; sharedInbox: string; };
|
||||
|
||||
const mockRemoteUser2: MiRemoteUser & { inbox: string; } = {
|
||||
id: 'remote-user-2',
|
||||
host: 'another.example.com',
|
||||
inbox: 'https://another.example.com/inbox',
|
||||
sharedInbox: null,
|
||||
} as MiRemoteUser & { inbox: string; };
|
||||
|
||||
const mockActivity: IActivity = {
|
||||
type: 'Create',
|
||||
id: 'activity-id',
|
||||
actor: 'https://local.example.com/users/local-user-id',
|
||||
object: {
|
||||
type: 'Note',
|
||||
id: 'note-id',
|
||||
content: 'Hello, world!',
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApDeliverManagerService,
|
||||
{
|
||||
provide: DI.followingsRepository,
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: QueueService,
|
||||
useValue: {
|
||||
deliverMany: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ApLoggerService,
|
||||
useValue: {
|
||||
logger: {
|
||||
createSubLogger: jest.fn().mockReturnValue({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ApDeliverManagerService>(ApDeliverManagerService);
|
||||
followingsRepository = module.get(DI.followingsRepository);
|
||||
queueService = module.get(QueueService);
|
||||
apLoggerService = module.get(ApLoggerService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('deliverToFollowers', () => {
|
||||
it('should deliver activity to all followers', async () => {
|
||||
const mockFollowings = [
|
||||
{
|
||||
followerSharedInbox: 'https://remote1.example.com/shared-inbox',
|
||||
followerInbox: 'https://remote1.example.com/inbox',
|
||||
},
|
||||
{
|
||||
followerSharedInbox: 'https://remote2.example.com/shared-inbox',
|
||||
followerInbox: 'https://remote2.example.com/inbox',
|
||||
},
|
||||
{
|
||||
followerSharedInbox: null,
|
||||
followerInbox: 'https://remote3.example.com/inbox',
|
||||
},
|
||||
];
|
||||
|
||||
followingsRepository.find.mockResolvedValue(mockFollowings as any);
|
||||
|
||||
await service.deliverToFollowers(mockLocalUser, mockActivity);
|
||||
|
||||
expect(followingsRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
followeeId: mockLocalUser.id,
|
||||
followerHost: expect.anything(), // Not(IsNull())
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(queueService.deliverMany).toHaveBeenCalledWith(
|
||||
{ id: mockLocalUser.id },
|
||||
mockActivity,
|
||||
expect.any(Map),
|
||||
);
|
||||
|
||||
// 呼び出されたinboxesを確認
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(3);
|
||||
expect(inboxes.has('https://remote1.example.com/shared-inbox')).toBe(true);
|
||||
expect(inboxes.has('https://remote2.example.com/shared-inbox')).toBe(true);
|
||||
expect(inboxes.has('https://remote3.example.com/inbox')).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude suspended followers by default', async () => {
|
||||
followingsRepository.find.mockResolvedValue([]);
|
||||
|
||||
await service.deliverToFollowers(mockLocalUser, mockActivity);
|
||||
|
||||
expect(followingsRepository.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
isFollowerSuspended: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deliverToUser', () => {
|
||||
it('should deliver activity to specific remote user', async () => {
|
||||
await service.deliverToUser(mockLocalUser, mockActivity, mockRemoteUser1);
|
||||
|
||||
expect(queueService.deliverMany).toHaveBeenCalledWith(
|
||||
{ id: mockLocalUser.id },
|
||||
mockActivity,
|
||||
expect.any(Map),
|
||||
);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(1);
|
||||
expect(inboxes.has(mockRemoteUser1.inbox)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle user without shared inbox', async () => {
|
||||
await service.deliverToUser(mockLocalUser, mockActivity, mockRemoteUser2);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(1);
|
||||
expect(inboxes.has(mockRemoteUser2.inbox)).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip user with null inbox', async () => {
|
||||
const userWithoutInbox = {
|
||||
...mockRemoteUser1,
|
||||
inbox: null,
|
||||
} as MiRemoteUser;
|
||||
|
||||
await service.deliverToUser(mockLocalUser, mockActivity, userWithoutInbox);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deliverToUsers', () => {
|
||||
it('should deliver activity to multiple remote users', async () => {
|
||||
await service.deliverToUsers(mockLocalUser, mockActivity, [mockRemoteUser1, mockRemoteUser2]);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has(mockRemoteUser1.inbox)).toBe(true);
|
||||
expect(inboxes.has(mockRemoteUser2.inbox)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDeliverManager', () => {
|
||||
it('should create a DeliverManager instance', () => {
|
||||
const manager = service.createDeliverManager(mockLocalUser, mockActivity);
|
||||
|
||||
expect(manager).toBeDefined();
|
||||
expect(typeof manager.addFollowersRecipe).toBe('function');
|
||||
expect(typeof manager.addDirectRecipe).toBe('function');
|
||||
expect(typeof manager.addAllKnowingSharedInboxRecipe).toBe('function');
|
||||
expect(typeof manager.execute).toBe('function');
|
||||
});
|
||||
|
||||
it('should allow manual recipe management', async () => {
|
||||
const manager = service.createDeliverManager(mockLocalUser, mockActivity);
|
||||
|
||||
followingsRepository.find.mockResolvedValue([
|
||||
{
|
||||
followerSharedInbox: null,
|
||||
followerInbox: 'https://follower.example.com/inbox',
|
||||
},
|
||||
] as any);
|
||||
|
||||
// フォロワー配信のレシピを追加
|
||||
manager.addFollowersRecipe();
|
||||
// ダイレクト配信のレシピを追加
|
||||
manager.addDirectRecipe(mockRemoteUser1);
|
||||
|
||||
await manager.execute();
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has('https://follower.example.com/inbox')).toBe(true);
|
||||
expect(inboxes.has(mockRemoteUser1.inbox)).toBe(true);
|
||||
});
|
||||
|
||||
it('should support ignoreSuspend option', async () => {
|
||||
const manager = service.createDeliverManager(mockLocalUser, mockActivity);
|
||||
|
||||
followingsRepository.find.mockResolvedValue([]);
|
||||
|
||||
manager.addFollowersRecipe();
|
||||
await manager.execute({ ignoreSuspend: true });
|
||||
|
||||
expect(followingsRepository.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
isFollowerSuspended: undefined, // ignoreSuspend: true なので undefined
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('followers and directs mixture: 先にfollowersでsharedInboxが追加されていた場合、directsでユーザーがそのsharedInboxを持っていたらinboxを追加しない', async () => {
|
||||
const manager = service.createDeliverManager(mockLocalUser, mockActivity);
|
||||
followingsRepository.find.mockResolvedValue([
|
||||
{
|
||||
followerSharedInbox: mockRemoteUser1.sharedInbox,
|
||||
followerInbox: mockRemoteUser2.inbox,
|
||||
},
|
||||
] as any);
|
||||
manager.addFollowersRecipe();
|
||||
manager.addDirectRecipe(mockRemoteUser1);
|
||||
await manager.execute();
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(1);
|
||||
expect(inboxes.has(mockRemoteUser1.sharedInbox)).toBe(true);
|
||||
expect(inboxes.has(mockRemoteUser1.inbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error for non-local actor', () => {
|
||||
const remoteActor = { id: 'remote-id', host: 'remote.example.com' } as any;
|
||||
|
||||
expect(() => {
|
||||
service.createDeliverManager(remoteActor, mockActivity);
|
||||
}).toThrow('actor.host must be null');
|
||||
});
|
||||
|
||||
it('should throw error when follower has null inbox', async () => {
|
||||
const mockFollowings = [
|
||||
{
|
||||
followerSharedInbox: null,
|
||||
followerInbox: null, // null inbox
|
||||
},
|
||||
];
|
||||
|
||||
followingsRepository.find.mockResolvedValue(mockFollowings as any);
|
||||
|
||||
await expect(service.deliverToFollowers(mockLocalUser, mockActivity)).rejects.toThrow('inbox is null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AllKnowingSharedInbox recipe', () => {
|
||||
it('should collect all shared inboxes when using AllKnowingSharedInbox', async () => {
|
||||
const mockQueryBuilder = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
orWhere: jest.fn().mockReturnThis(),
|
||||
distinct: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn<any>().mockResolvedValue([
|
||||
{ f_followerSharedInbox: 'https://shared1.example.com/inbox' },
|
||||
{ f_followeeSharedInbox: 'https://shared2.example.com/inbox' },
|
||||
]),
|
||||
};
|
||||
|
||||
followingsRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
|
||||
|
||||
const manager = service.createDeliverManager(mockLocalUser, mockActivity);
|
||||
manager.addAllKnowingSharedInboxRecipe();
|
||||
|
||||
await manager.execute();
|
||||
|
||||
expect(followingsRepository.createQueryBuilder).toHaveBeenCalledWith('f');
|
||||
expect(mockQueryBuilder.select).toHaveBeenCalledWith([
|
||||
'f.followerSharedInbox',
|
||||
'f.followeeSharedInbox',
|
||||
]);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has('https://shared1.example.com/inbox')).toBe(true);
|
||||
expect(inboxes.has('https://shared2.example.com/inbox')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApDeliverManagerService (SQL)', () => {
|
||||
// followerにデータを挿入して、SQLの動作を確認します
|
||||
let app: TestingModule;
|
||||
let service: ApDeliverManagerService;
|
||||
let followingsRepository: FollowingsRepository;
|
||||
let usersRepository: UsersRepository;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
|
||||
async function createUser(data: Partial<{ id: string; username: string; host: string | null; inbox: string | null; sharedInbox: string | null; isSuspended: boolean }> = {}): Promise<any> {
|
||||
const user = {
|
||||
id: secureRndstr(16),
|
||||
username: secureRndstr(16),
|
||||
usernameLower: (data.username ?? secureRndstr(16)).toLowerCase(),
|
||||
host: data.host ?? null,
|
||||
inbox: data.inbox ?? null,
|
||||
sharedInbox: data.sharedInbox ?? null,
|
||||
isSuspended: data.isSuspended ?? false,
|
||||
...data,
|
||||
};
|
||||
|
||||
await usersRepository.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createFollowing(follower: any, followee: any, data: Partial<{
|
||||
followerInbox: string | null;
|
||||
followerSharedInbox: string | null;
|
||||
followeeInbox: string | null;
|
||||
followeeSharedInbox: string | null;
|
||||
isFollowerSuspended: boolean;
|
||||
}> = {}): Promise<any> {
|
||||
const following = {
|
||||
id: secureRndstr(16),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
followerHost: follower.host,
|
||||
followeeHost: followee.host,
|
||||
followerInbox: data.followerInbox ?? follower.inbox,
|
||||
followerSharedInbox: data.followerSharedInbox ?? follower.sharedInbox,
|
||||
followeeInbox: data.followeeInbox ?? null,
|
||||
followeeSharedInbox: data.followeeSharedInbox ?? null,
|
||||
isFollowerSuspended: data.isFollowerSuspended ?? false,
|
||||
isFollowerHibernated: false,
|
||||
withReplies: false,
|
||||
notify: null,
|
||||
};
|
||||
|
||||
await followingsRepository.insert(following);
|
||||
return following;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { Test } = await import('@nestjs/testing');
|
||||
const { GlobalModule } = await import('@/GlobalModule.js');
|
||||
const { DI } = await import('@/di-symbols.js');
|
||||
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule],
|
||||
providers: [
|
||||
ApDeliverManagerService,
|
||||
{
|
||||
provide: QueueService,
|
||||
useFactory: () => ({
|
||||
deliverMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ApLoggerService,
|
||||
useValue: {
|
||||
logger: {
|
||||
createSubLogger: jest.fn().mockReturnValue({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
||||
service = app.get<ApDeliverManagerService>(ApDeliverManagerService);
|
||||
followingsRepository = app.get<FollowingsRepository>(DI.followingsRepository);
|
||||
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('deliverToFollowers with real data', () => {
|
||||
it('should deliver to followers excluding suspended ones', async () => {
|
||||
// Create local user (followee)
|
||||
const localUser = await createUser({
|
||||
host: null,
|
||||
username: 'localuser',
|
||||
});
|
||||
|
||||
// Create remote followers
|
||||
const activeFollower = await createUser({
|
||||
host: 'active.example.com',
|
||||
username: 'activefollower',
|
||||
inbox: 'https://active.example.com/inbox',
|
||||
sharedInbox: 'https://active.example.com/shared-inbox',
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
const suspendedFollower = await createUser({
|
||||
host: 'suspended.example.com',
|
||||
username: 'suspendedfollower',
|
||||
inbox: 'https://suspended.example.com/inbox',
|
||||
sharedInbox: 'https://suspended.example.com/shared-inbox',
|
||||
isSuspended: true,
|
||||
});
|
||||
|
||||
const followerWithoutSharedInbox = await createUser({
|
||||
host: 'noshared.example.com',
|
||||
username: 'noshared',
|
||||
inbox: 'https://noshared.example.com/inbox',
|
||||
sharedInbox: null,
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
// Create following relationships
|
||||
await createFollowing(activeFollower, localUser, {
|
||||
followerInbox: activeFollower.inbox,
|
||||
followerSharedInbox: activeFollower.sharedInbox,
|
||||
isFollowerSuspended: false,
|
||||
});
|
||||
|
||||
await createFollowing(suspendedFollower, localUser, {
|
||||
followerInbox: suspendedFollower.inbox,
|
||||
followerSharedInbox: suspendedFollower.sharedInbox,
|
||||
isFollowerSuspended: true, // 凍結されたフォロワー
|
||||
});
|
||||
|
||||
await createFollowing(followerWithoutSharedInbox, localUser, {
|
||||
followerInbox: followerWithoutSharedInbox.inbox,
|
||||
followerSharedInbox: null,
|
||||
isFollowerSuspended: false,
|
||||
});
|
||||
|
||||
const mockActivity = {
|
||||
type: 'Create',
|
||||
id: 'test-activity',
|
||||
actor: `https://local.example.com/users/${localUser.id}`,
|
||||
object: { type: 'Note', content: 'Hello' },
|
||||
} as any;
|
||||
|
||||
// Execute delivery
|
||||
await service.deliverToFollowers(localUser, mockActivity);
|
||||
|
||||
// Verify delivery was queued
|
||||
expect(queueService.deliverMany).toHaveBeenCalledTimes(1);
|
||||
const [actor, activity, inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
|
||||
expect(actor.id).toBe(localUser.id);
|
||||
expect(activity).toBe(mockActivity);
|
||||
|
||||
// Check inboxes - should include active followers but exclude suspended ones
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has('https://active.example.com/shared-inbox')).toBe(true);
|
||||
expect(inboxes.has('https://noshared.example.com/inbox')).toBe(true);
|
||||
expect(inboxes.has('https://suspended.example.com/shared-inbox')).toBe(false);
|
||||
});
|
||||
|
||||
it('should include suspended followers when ignoreSuspend is true', async () => {
|
||||
const localUser = await createUser({ host: null });
|
||||
const suspendedFollower = await createUser({
|
||||
host: 'suspended.example.com',
|
||||
inbox: 'https://suspended.example.com/inbox',
|
||||
isSuspended: true,
|
||||
});
|
||||
|
||||
await createFollowing(suspendedFollower, localUser, {
|
||||
isFollowerSuspended: true,
|
||||
});
|
||||
|
||||
const manager = service.createDeliverManager(localUser, { type: 'Test' } as any);
|
||||
manager.addFollowersRecipe();
|
||||
|
||||
// Execute with ignoreSuspend: true
|
||||
await manager.execute({ ignoreSuspend: true });
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
expect(inboxes.size).toBe(1);
|
||||
expect(inboxes.has('https://suspended.example.com/inbox')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed follower types correctly', async () => {
|
||||
const localUser = await createUser({ host: null });
|
||||
|
||||
// フォロワー1: shared inbox あり
|
||||
const follower1 = await createUser({
|
||||
host: 'server1.example.com',
|
||||
inbox: 'https://server1.example.com/users/user1/inbox',
|
||||
sharedInbox: 'https://server1.example.com/inbox',
|
||||
});
|
||||
|
||||
// フォロワー2: 同じサーバーの別ユーザー(shared inbox は同じ)
|
||||
const follower2 = await createUser({
|
||||
host: 'server1.example.com',
|
||||
inbox: 'https://server1.example.com/users/user2/inbox',
|
||||
sharedInbox: 'https://server1.example.com/inbox',
|
||||
});
|
||||
|
||||
// フォロワー3: 別サーバー、shared inbox なし
|
||||
const follower3 = await createUser({
|
||||
host: 'server2.example.com',
|
||||
inbox: 'https://server2.example.com/users/user3/inbox',
|
||||
sharedInbox: null,
|
||||
});
|
||||
|
||||
await createFollowing(follower1, localUser);
|
||||
await createFollowing(follower2, localUser);
|
||||
await createFollowing(follower3, localUser);
|
||||
|
||||
await service.deliverToFollowers(localUser, { type: 'Test' } as any);
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
|
||||
// shared inbox は重複排除されるので、2つのinboxのみ
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has('https://server1.example.com/inbox')).toBe(true); // shared inbox
|
||||
expect(inboxes.has('https://server2.example.com/users/user3/inbox')).toBe(true); // individual inbox
|
||||
|
||||
// individual inbox は shared inbox があるので使用されない
|
||||
expect(inboxes.has('https://server1.example.com/users/user1/inbox')).toBe(false);
|
||||
expect(inboxes.has('https://server1.example.com/users/user2/inbox')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AllKnowingSharedInbox with real data', () => {
|
||||
it('should collect all unique shared inboxes from database', async () => {
|
||||
// Create users with various inbox configurations
|
||||
const user1 = await createUser({ host: null });
|
||||
const user2 = await createUser({ host: null });
|
||||
|
||||
const remoteUser1 = await createUser({
|
||||
host: 'server1.example.com',
|
||||
sharedInbox: 'https://server1.example.com/shared',
|
||||
});
|
||||
|
||||
const remoteUser2 = await createUser({
|
||||
host: 'server2.example.com',
|
||||
sharedInbox: 'https://server2.example.com/shared',
|
||||
});
|
||||
|
||||
const remoteUser3 = await createUser({
|
||||
host: 'server1.example.com', // 同じサーバー
|
||||
sharedInbox: 'https://server1.example.com/shared', // 同じ shared inbox
|
||||
});
|
||||
|
||||
// Create following relationships
|
||||
await createFollowing(remoteUser1, user1, {
|
||||
followerSharedInbox: 'https://server1.example.com/shared',
|
||||
});
|
||||
|
||||
await createFollowing(user1, remoteUser2, {
|
||||
followerSharedInbox: null,
|
||||
followeeSharedInbox: 'https://server2.example.com/shared',
|
||||
});
|
||||
|
||||
await createFollowing(remoteUser3, user2, {
|
||||
followerSharedInbox: 'https://server1.example.com/shared', // 重複
|
||||
});
|
||||
|
||||
const manager = service.createDeliverManager(user1, { type: 'Test' } as any);
|
||||
manager.addAllKnowingSharedInboxRecipe();
|
||||
|
||||
await manager.execute();
|
||||
|
||||
const [, , inboxes] = queueService.deliverMany.mock.calls[0];
|
||||
|
||||
// 重複は除去されて2つのユニークな shared inbox
|
||||
expect(inboxes.size).toBe(2);
|
||||
expect(inboxes.has('https://server1.example.com/shared')).toBe(true);
|
||||
expect(inboxes.has('https://server2.example.com/shared')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import {
|
||||
MiFollowing,
|
||||
MiUser,
|
||||
FollowingsRepository,
|
||||
FollowRequestsRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { randomString } from '../utils.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
|
||||
|
||||
function genHost() {
|
||||
return randomString() + '.example.com';
|
||||
}
|
||||
|
||||
describe('UserSuspendService', () => {
|
||||
let app: TestingModule;
|
||||
let userSuspendService: UserSuspendService;
|
||||
let usersRepository: UsersRepository;
|
||||
let followingsRepository: FollowingsRepository;
|
||||
let followRequestsRepository: FollowRequestsRepository;
|
||||
let userEntityService: jest.Mocked<UserEntityService>;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let globalEventService: jest.Mocked<GlobalEventService>;
|
||||
let apRendererService: jest.Mocked<ApRendererService>;
|
||||
let moderationLogService: jest.Mocked<ModerationLogService>;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}): Promise<MiUser> {
|
||||
const user = {
|
||||
id: secureRndstr(16),
|
||||
username: secureRndstr(16),
|
||||
usernameLower: secureRndstr(16).toLowerCase(),
|
||||
host: null,
|
||||
isSuspended: false,
|
||||
...data,
|
||||
} as MiUser;
|
||||
|
||||
await usersRepository.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createFollowing(follower: MiUser, followee: MiUser, data: Partial<MiFollowing> = {}): Promise<MiFollowing> {
|
||||
const following = {
|
||||
id: secureRndstr(16),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
isFollowerSuspended: false,
|
||||
isFollowerHibernated: false,
|
||||
withReplies: false,
|
||||
notify: null,
|
||||
followerHost: follower.host,
|
||||
followerInbox: null,
|
||||
followerSharedInbox: null,
|
||||
followeeHost: followee.host,
|
||||
followeeInbox: null,
|
||||
followeeSharedInbox: null,
|
||||
...data,
|
||||
} as MiFollowing;
|
||||
|
||||
await followingsRepository.insert(following);
|
||||
return following;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule],
|
||||
providers: [
|
||||
UserSuspendService,
|
||||
ApDeliverManagerService,
|
||||
{
|
||||
provide: UserEntityService,
|
||||
useFactory: () => ({
|
||||
isLocalUser: jest.fn(),
|
||||
genLocalUserUri: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: QueueService,
|
||||
useFactory: () => ({
|
||||
deliverMany: jest.fn(),
|
||||
deliver: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: GlobalEventService,
|
||||
useFactory: () => ({
|
||||
publishInternalEvent: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ModerationLogService,
|
||||
useFactory: () => ({
|
||||
log: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: RelayService,
|
||||
useFactory: () => ({
|
||||
deliverToRelays: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ApRendererService,
|
||||
useFactory: () => ({
|
||||
renderDelete: jest.fn(),
|
||||
renderUndo: jest.fn(),
|
||||
renderPerson: jest.fn(),
|
||||
renderUpdate: jest.fn(),
|
||||
addContext: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ApLoggerService,
|
||||
useFactory: () => ({
|
||||
logger: {
|
||||
createSubLogger: jest.fn().mockReturnValue({
|
||||
info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
||||
userSuspendService = app.get<UserSuspendService>(UserSuspendService);
|
||||
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||
followingsRepository = app.get<FollowingsRepository>(DI.followingsRepository);
|
||||
followRequestsRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
|
||||
userEntityService = app.get<UserEntityService>(UserEntityService) as jest.Mocked<UserEntityService>;
|
||||
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
||||
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
|
||||
apRendererService = app.get<ApRendererService>(ApRendererService) as jest.Mocked<ApRendererService>;
|
||||
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('suspend', () => {
|
||||
test('should suspend user and update database', async () => {
|
||||
const user = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
|
||||
// ユーザーが凍結されているかチェック
|
||||
const suspendedUser = await usersRepository.findOneBy({ id: user.id });
|
||||
expect(suspendedUser?.isSuspended).toBe(true);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'suspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
|
||||
test('should mark follower relationships as suspended', async () => {
|
||||
const user = await createUser();
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// ユーザーがフォローしている関係を作成
|
||||
await createFollowing(user, followee1);
|
||||
await createFollowing(user, followee2);
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// フォロー関係が論理削除されているかチェック
|
||||
const followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should publish internal event for suspension', async () => {
|
||||
const user = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 内部イベントが発行されているかチェック(非同期処理のため少し待つ)
|
||||
await setTimeout(100);
|
||||
|
||||
expect(globalEventService.publishInternalEvent).toHaveBeenCalledWith(
|
||||
'userChangeSuspendedState',
|
||||
{ id: user.id, isSuspended: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuspend', () => {
|
||||
test('should unsuspend user and update database', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーの凍結が解除されているかチェック
|
||||
const unsuspendedUser = await usersRepository.findOneBy({ id: user.id });
|
||||
expect(unsuspendedUser?.isSuspended).toBe(false);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'unsuspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
|
||||
test('should restore follower relationships', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// 凍結状態のフォロー関係を作成
|
||||
await createFollowing(user, followee1, { isFollowerSuspended: true });
|
||||
await createFollowing(user, followee2, { isFollowerSuspended: true });
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// フォロー関係が復元されているかチェック
|
||||
const followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should publish internal event for unsuspension', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 内部イベントが発行されているかチェック(非同期処理のため少し待つ)
|
||||
await setTimeout(100);
|
||||
|
||||
expect(globalEventService.publishInternalEvent).toHaveBeenCalledWith(
|
||||
'userChangeSuspendedState',
|
||||
{ id: user.id, isSuspended: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test: suspend and unsuspend cycle', () => {
|
||||
test('should preserve follow relationships through suspend/unsuspend cycle', async () => {
|
||||
const user = await createUser();
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// 初期のフォロー関係を作成
|
||||
await createFollowing(user, followee1);
|
||||
await createFollowing(user, followee2);
|
||||
|
||||
// 初期状態の確認
|
||||
let followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
|
||||
// 凍結
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 凍結後の状態確認
|
||||
followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(true);
|
||||
});
|
||||
|
||||
// 凍結解除
|
||||
const suspendedUser = await usersRepository.findOneByOrFail({ id: user.id });
|
||||
await userSuspendService.unsuspend(suspendedUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 凍結解除後の状態確認
|
||||
followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActivityPub delivery', () => {
|
||||
test('should deliver Delete activity on suspend of local user', async () => {
|
||||
const localUser = await createUser({ host: null });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(true);
|
||||
userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`);
|
||||
apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any);
|
||||
apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Delete' } as any);
|
||||
|
||||
await userSuspendService.suspend(localUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれているかチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser);
|
||||
expect(apRendererService.renderDelete).toHaveBeenCalled();
|
||||
expect(apRendererService.addContext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should deliver Undo Delete activity on unsuspend of local user', async () => {
|
||||
const localUser = await createUser({ host: null, isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(true);
|
||||
userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`);
|
||||
apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any);
|
||||
apRendererService.renderUndo.mockReturnValue({ type: 'Undo' } as any);
|
||||
apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Undo' } as any);
|
||||
|
||||
await userSuspendService.unsuspend(localUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれているかチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser);
|
||||
expect(apRendererService.renderDelete).toHaveBeenCalled();
|
||||
expect(apRendererService.renderUndo).toHaveBeenCalled();
|
||||
expect(apRendererService.addContext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not deliver any activity on suspend of remote user', async () => {
|
||||
const remoteUser = await createUser({ host: 'remote.example.com' });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(false);
|
||||
|
||||
await userSuspendService.suspend(remoteUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれていないことをチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(remoteUser);
|
||||
expect(apRendererService.renderDelete).not.toHaveBeenCalled();
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote user suspension', () => {
|
||||
test('should suspend remote user without AP delivery', async () => {
|
||||
const remoteUser = await createUser({ host: genHost() });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(remoteUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーが凍結されているかチェック
|
||||
const suspendedUser = await usersRepository.findOneBy({ id: remoteUser.id });
|
||||
expect(suspendedUser?.isSuspended).toBe(true);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'suspend', {
|
||||
userId: remoteUser.id,
|
||||
userUsername: remoteUser.username,
|
||||
userHost: remoteUser.host,
|
||||
});
|
||||
|
||||
// ActivityPub配信が呼ばれていないことを確認
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote user unsuspension', () => {
|
||||
test('should unsuspend remote user without AP delivery', async () => {
|
||||
const remoteUser = await createUser({ host: genHost(), isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(remoteUser, moderator);
|
||||
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーの凍結が解除されているかチェック
|
||||
const unsuspendedUser = await usersRepository.findOneBy({ id: remoteUser.id });
|
||||
expect(unsuspendedUser?.isSuspended).toBe(false);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'unsuspend', {
|
||||
userId: remoteUser.id,
|
||||
userUsername: remoteUser.username,
|
||||
userHost: remoteUser.host,
|
||||
});
|
||||
|
||||
// ActivityPub配信が呼ばれていないことを確認
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -64,6 +64,8 @@ function toBase62(n: number): string {
|
|||
}
|
||||
|
||||
export function getConfig(): UserConfig {
|
||||
const localesHash = toBase62(hash(JSON.stringify(locales)));
|
||||
|
||||
return {
|
||||
base: '/embed_vite/',
|
||||
|
||||
|
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
|
|||
// dependencies of i18n.ts
|
||||
'config': ['@@/js/config.js'],
|
||||
},
|
||||
entryFileNames: 'scripts/[hash:8].js',
|
||||
chunkFileNames: 'scripts/[hash:8].js',
|
||||
assetFileNames: 'assets/[hash:8][extname]',
|
||||
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
|
||||
paths(id) {
|
||||
for (const p of externalPackages) {
|
||||
if (p.match.test(id)) {
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"json5": "2.2.3",
|
||||
"magic-string": "0.30.18",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.15.1",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { initShaderProgram } from '@/utility/webgl.js';
|
||||
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
|
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
|
|||
focus: 1.0,
|
||||
});
|
||||
|
||||
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
|
||||
const shader = gl.createShader(type);
|
||||
if (shader == null) return null;
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
alert(
|
||||
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
|
||||
);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
if (vertexShader == null || fragmentShader == null) return null;
|
||||
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
|
||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||
alert(
|
||||
`failed to init shader: ${gl.getProgramInfoLog(
|
||||
shaderProgram,
|
||||
)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -71,7 +31,7 @@ onMounted(() => {
|
|||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
||||
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
|
||||
if (maybeGl == null) return;
|
||||
|
||||
const gl = maybeGl;
|
||||
|
@ -82,18 +42,16 @@ onMounted(() => {
|
|||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `
|
||||
attribute vec2 vertex;
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `#version 300 es
|
||||
in vec2 position;
|
||||
uniform vec2 u_scale;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertex, 0.0, 1.0);
|
||||
v_pos = vertex / u_scale;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
in_uv = position / u_scale;
|
||||
}
|
||||
`, `
|
||||
`, `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
vec3 mod289(vec3 x) {
|
||||
|
@ -143,6 +101,7 @@ onMounted(() => {
|
|||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_spread;
|
||||
|
@ -150,8 +109,7 @@ onMounted(() => {
|
|||
uniform float u_warp;
|
||||
uniform float u_focus;
|
||||
uniform float u_itensity;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec4 out_color;
|
||||
|
||||
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
||||
float SPREAD = 0.7 * u_spread;
|
||||
|
@ -182,13 +140,13 @@ onMounted(() => {
|
|||
|
||||
float ratio = u_resolution.x / u_resolution.y;
|
||||
|
||||
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
|
||||
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
|
||||
|
||||
vec3 color = vec3( 0.0 );
|
||||
|
||||
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
|
||||
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
|
@ -198,10 +156,10 @@ onMounted(() => {
|
|||
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
|
||||
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
|
||||
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
|
||||
vec3 inverted = vec3( 1.0 ) - color;
|
||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
||||
out_color = vec4(color, max(max(color.x, color.y), color.z));
|
||||
}
|
||||
`);
|
||||
if (shaderProgram == null) return;
|
||||
|
@ -223,7 +181,7 @@ onMounted(() => {
|
|||
gl.uniform1f(u_itensity, 0.5);
|
||||
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
||||
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'position');
|
||||
gl.enableVertexAttribArray(vertex);
|
||||
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
|
|
|
@ -530,6 +530,14 @@ defineExpose({
|
|||
--eachSize: 50px;
|
||||
}
|
||||
|
||||
&.s4 {
|
||||
--eachSize: 55px;
|
||||
}
|
||||
|
||||
&.s5 {
|
||||
--eachSize: 60px;
|
||||
}
|
||||
|
||||
&.w1 {
|
||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
|
|
|
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
|
||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
|
@ -218,6 +218,10 @@ const uploader = useUploader({
|
|||
multiple: true,
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uploader.dispose();
|
||||
});
|
||||
|
||||
uploader.events.on('itemUploaded', ctx => {
|
||||
files.value.push(ctx.item.uploaded!);
|
||||
uploader.removeItem(ctx.item);
|
||||
|
@ -1300,6 +1304,7 @@ async function canClose() {
|
|||
|
||||
defineExpose({
|
||||
clear,
|
||||
abortUploader: () => uploader.abortAll(),
|
||||
canClose,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -54,6 +54,7 @@ function onPosted() {
|
|||
async function _close() {
|
||||
const canClose = await form.value?.canClose();
|
||||
if (!canClose) return;
|
||||
form.value?.abortUploader();
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:key="item.id"
|
||||
v-panel
|
||||
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
|
||||
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
|
||||
:style="{
|
||||
'--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%',
|
||||
'--pp': item.preprocessProgress != null ? `${item.preprocessProgress * 100}%` : '100%',
|
||||
}"
|
||||
@contextmenu.prevent.stop="onContextmenu(item, $event)"
|
||||
>
|
||||
<div :class="$style.itemInner">
|
||||
|
@ -19,11 +22,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
|
||||
<div :class="$style.itemBody">
|
||||
<div><i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
|
||||
<div>
|
||||
<i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i>
|
||||
<MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine>
|
||||
</div>
|
||||
<div :class="$style.itemInfo">
|
||||
<span>{{ item.file.type }}</span>
|
||||
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
|
||||
<span v-else>{{ bytes(item.file.size) }}</span>
|
||||
<span v-if="item.preprocessing">{{ i18n.ts.preprocessing }}<MkLoading inline em style="margin-left: 0.5em;"/></span>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
|
@ -97,7 +104,7 @@ function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: var(--pp, 100%);
|
||||
height: 100%;
|
||||
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
||||
background-size: 25px 25px;
|
||||
|
|
|
@ -43,6 +43,12 @@ const IMAGE_EDITING_SUPPORTED_TYPES = [
|
|||
'image/webp',
|
||||
];
|
||||
|
||||
const VIDEO_COMPRESSION_SUPPORTED_TYPES = [ // TODO
|
||||
'video/mp4',
|
||||
'video/quicktime',
|
||||
'video/x-matroska',
|
||||
];
|
||||
|
||||
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
|
||||
|
||||
const IMAGE_PREPROCESS_NEEDED_TYPES = [
|
||||
|
@ -51,6 +57,10 @@ const IMAGE_PREPROCESS_NEEDED_TYPES = [
|
|||
...IMAGE_EDITING_SUPPORTED_TYPES,
|
||||
];
|
||||
|
||||
const VIDEO_PREPROCESS_NEEDED_TYPES = [
|
||||
...VIDEO_COMPRESSION_SUPPORTED_TYPES,
|
||||
];
|
||||
|
||||
const mimeTypeMap = {
|
||||
'image/webp': 'webp',
|
||||
'image/jpeg': 'jpg',
|
||||
|
@ -64,6 +74,7 @@ export type UploaderItem = {
|
|||
progress: { max: number; value: number } | null;
|
||||
thumbnail: string | null;
|
||||
preprocessing: boolean;
|
||||
preprocessProgress: number | null;
|
||||
uploading: boolean;
|
||||
uploaded: Misskey.entities.DriveFile | null;
|
||||
uploadFailed: boolean;
|
||||
|
@ -76,6 +87,7 @@ export type UploaderItem = {
|
|||
isSensitive?: boolean;
|
||||
caption?: string | null;
|
||||
abort?: (() => void) | null;
|
||||
abortPreprocess?: (() => void) | null;
|
||||
};
|
||||
|
||||
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
|
||||
|
@ -129,11 +141,12 @@ export function useUploader(options: {
|
|||
progress: null,
|
||||
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
|
||||
preprocessing: false,
|
||||
preprocessProgress: null,
|
||||
uploading: false,
|
||||
aborted: false,
|
||||
uploaded: null,
|
||||
uploadFailed: false,
|
||||
compressionLevel: prefer.s.defaultImageCompressionLevel,
|
||||
compressionLevel: IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultImageCompressionLevel : VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultVideoCompressionLevel : 0,
|
||||
watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null,
|
||||
file: markRaw(file),
|
||||
});
|
||||
|
@ -318,7 +331,7 @@ export function useUploader(options: {
|
|||
}
|
||||
|
||||
if (
|
||||
IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||
(IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) || VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type)) &&
|
||||
!item.preprocessing &&
|
||||
!item.uploading &&
|
||||
!item.uploaded
|
||||
|
@ -391,6 +404,19 @@ export function useUploader(options: {
|
|||
removeItem(item);
|
||||
},
|
||||
});
|
||||
} else if (item.preprocessing && item.abortPreprocess != null) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
icon: 'ti ti-player-stop',
|
||||
text: i18n.ts.abort,
|
||||
danger: true,
|
||||
action: () => {
|
||||
if (item.abortPreprocess != null) {
|
||||
item.abortPreprocess();
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (item.uploading) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
|
@ -474,6 +500,10 @@ export function useUploader(options: {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (item.abortPreprocess != null) {
|
||||
item.abortPreprocess();
|
||||
}
|
||||
|
||||
if (item.abort != null) {
|
||||
item.abort();
|
||||
}
|
||||
|
@ -484,18 +514,30 @@ export function useUploader(options: {
|
|||
|
||||
async function preprocess(item: UploaderItem): Promise<void> {
|
||||
item.preprocessing = true;
|
||||
item.preprocessProgress = null;
|
||||
|
||||
try {
|
||||
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
|
||||
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
|
||||
try {
|
||||
await preprocessForImage(item);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to preprocess image', err);
|
||||
} catch (err) {
|
||||
console.error('Failed to preprocess image', err);
|
||||
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
||||
if (VIDEO_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
|
||||
try {
|
||||
await preprocessForVideo(item);
|
||||
} catch (err) {
|
||||
console.error('Failed to preprocess video', err);
|
||||
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
||||
item.preprocessing = false;
|
||||
item.preprocessProgress = null;
|
||||
}
|
||||
|
||||
async function preprocessForImage(item: UploaderItem): Promise<void> {
|
||||
|
@ -564,10 +606,74 @@ export function useUploader(options: {
|
|||
item.preprocessedFile = markRaw(preprocessedFile);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
async function preprocessForVideo(item: UploaderItem): Promise<void> {
|
||||
let preprocessedFile: Blob | File = item.file;
|
||||
|
||||
const needsCompress = item.compressionLevel !== 0 && VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type);
|
||||
|
||||
if (needsCompress) {
|
||||
const mediabunny = await import('mediabunny');
|
||||
|
||||
const source = new mediabunny.BlobSource(preprocessedFile);
|
||||
|
||||
const input = new mediabunny.Input({
|
||||
source,
|
||||
formats: mediabunny.ALL_FORMATS,
|
||||
});
|
||||
|
||||
const output = new mediabunny.Output({
|
||||
target: new mediabunny.BufferTarget(),
|
||||
format: new mediabunny.Mp4OutputFormat(),
|
||||
});
|
||||
|
||||
const currentConversion = await mediabunny.Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
//width: 320, // Height will be deduced automatically to retain aspect ratio
|
||||
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
|
||||
},
|
||||
audio: {
|
||||
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
|
||||
},
|
||||
});
|
||||
|
||||
currentConversion.onProgress = newProgress => item.preprocessProgress = newProgress;
|
||||
|
||||
item.abortPreprocess = () => {
|
||||
item.abortPreprocess = null;
|
||||
currentConversion.cancel();
|
||||
item.preprocessing = false;
|
||||
item.preprocessProgress = null;
|
||||
};
|
||||
|
||||
await currentConversion.execute();
|
||||
|
||||
item.abortPreprocess = null;
|
||||
|
||||
preprocessedFile = new Blob([output.target.buffer!], { type: output.format.mimeType });
|
||||
item.compressedSize = output.target.buffer!.byteLength;
|
||||
item.uploadName = `${item.name}.mp4`;
|
||||
} else {
|
||||
item.compressedSize = null;
|
||||
item.uploadName = item.name;
|
||||
}
|
||||
|
||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
||||
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
|
||||
item.preprocessedFile = markRaw(preprocessedFile);
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
for (const item of items.value) {
|
||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
||||
}
|
||||
|
||||
abortAll();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -575,6 +681,7 @@ export function useUploader(options: {
|
|||
addFiles,
|
||||
removeItem,
|
||||
abortAll,
|
||||
dispose,
|
||||
upload,
|
||||
getMenu,
|
||||
uploading: computed(() => items.value.some(item => item.uploading)),
|
||||
|
|
|
@ -129,13 +129,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSelect
|
||||
v-model="defaultImageCompressionLevel" :items="[
|
||||
{ label: i18n.ts.none, value: 0 },
|
||||
{ label: i18n.ts.low, value: 1 },
|
||||
{ label: i18n.ts.medium, value: 2 },
|
||||
{ label: i18n.ts.high, value: 3 },
|
||||
{ label: `${i18n.ts.low} (${i18n.ts._compression._quality.high}; ${i18n.ts._compression._size.large})`, value: 1 },
|
||||
{ label: `${i18n.ts.medium} (${i18n.ts._compression._quality.medium}; ${i18n.ts._compression._size.medium})`, value: 2 },
|
||||
{ label: `${i18n.ts.high} (${i18n.ts._compression._quality.low}; ${i18n.ts._compression._size.small})`, value: 3 },
|
||||
]"
|
||||
>
|
||||
<template #label><SearchLabel>{{ i18n.ts.defaultImageCompressionLevel }}</SearchLabel></template>
|
||||
<template #caption><div v-html="i18n.ts.defaultImageCompressionLevel_description"></div></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts.defaultCompressionLevel }}</SearchLabel></template>
|
||||
<template #caption><div v-html="i18n.ts.defaultCompressionLevel_description"></div></template>
|
||||
</MkSelect>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</FormSection>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['video']">
|
||||
<FormSection>
|
||||
<template #label><SearchLabel>{{ i18n.ts.video }}</SearchLabel></template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<SearchMarker :keywords="['default', 'video', 'compression']">
|
||||
<MkPreferenceContainer k="defaultVideoCompressionLevel">
|
||||
<MkSelect
|
||||
v-model="defaultVideoCompressionLevel" :items="[
|
||||
{ label: i18n.ts.none, value: 0 },
|
||||
{ label: `${i18n.ts.low} (${i18n.ts._compression._quality.high}; ${i18n.ts._compression._size.large})`, value: 1 },
|
||||
{ label: `${i18n.ts.medium} (${i18n.ts._compression._quality.medium}; ${i18n.ts._compression._size.medium})`, value: 2 },
|
||||
{ label: `${i18n.ts.high} (${i18n.ts._compression._quality.low}; ${i18n.ts._compression._size.small})`, value: 3 },
|
||||
]"
|
||||
>
|
||||
<template #label><SearchLabel>{{ i18n.ts.defaultCompressionLevel }}</SearchLabel></template>
|
||||
<template #caption><div v-html="i18n.ts.defaultCompressionLevel_description"></div></template>
|
||||
</MkSelect>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
@ -196,6 +220,7 @@ const meterStyle = computed(() => {
|
|||
const keepOriginalFilename = prefer.model('keepOriginalFilename');
|
||||
const defaultWatermarkPresetId = prefer.model('defaultWatermarkPresetId');
|
||||
const defaultImageCompressionLevel = prefer.model('defaultImageCompressionLevel');
|
||||
const defaultVideoCompressionLevel = prefer.model('defaultVideoCompressionLevel');
|
||||
|
||||
const watermarkPresetsSyncEnabled = ref(prefer.isSyncEnabled('watermarkPresets'));
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option :value="1">{{ i18n.ts.small }}</option>
|
||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
||||
<option :value="3">{{ i18n.ts.large }}</option>
|
||||
<option :value="4">{{ i18n.ts.large }}+</option>
|
||||
<option :value="5">{{ i18n.ts.large }}++</option>
|
||||
</MkRadios>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
@ -95,11 +97,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<SearchMarker :keywords="['emoji', 'picker', 'style']">
|
||||
<MkPreferenceContainer k="emojiPickerStyle">
|
||||
<MkSelect v-model="emojiPickerStyle" :items="[
|
||||
{ label: i18n.ts.auto, value: 'auto' },
|
||||
{ label: i18n.ts.popup, value: 'popup' },
|
||||
{ label: i18n.ts.drawer, value: 'drawer' },
|
||||
]">
|
||||
<MkSelect
|
||||
v-model="emojiPickerStyle" :items="[
|
||||
{ label: i18n.ts.auto, value: 'auto' },
|
||||
{ label: i18n.ts.popup, value: 'popup' },
|
||||
{ label: i18n.ts.drawer, value: 'drawer' },
|
||||
]"
|
||||
>
|
||||
<template #label><SearchLabel>{{ i18n.ts.style }}</SearchLabel></template>
|
||||
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
|
||||
</MkSelect>
|
||||
|
@ -116,13 +120,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import XPalette from './emoji-palette.palette.vue';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
|
|
@ -439,6 +439,9 @@ export const PREF_DEF = definePreferences({
|
|||
defaultImageCompressionLevel: {
|
||||
default: 2 as 0 | 1 | 2 | 3,
|
||||
},
|
||||
defaultVideoCompressionLevel: {
|
||||
default: 2 as 0 | 1 | 2 | 3,
|
||||
},
|
||||
|
||||
'sound.masterVolume': {
|
||||
default: 0.5,
|
||||
|
|
|
@ -230,11 +230,16 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
icon: 'ti ti-search',
|
||||
text: i18n.ts.searchThisUsersNotes,
|
||||
action: () => {
|
||||
router.push('/search', {
|
||||
query: {
|
||||
const query = {
|
||||
username: user.username,
|
||||
host: user.host ?? undefined,
|
||||
},
|
||||
} as { username: string, host?: string };
|
||||
|
||||
if (user.host !== null) {
|
||||
query.host = user.host;
|
||||
}
|
||||
|
||||
router.push('/search', {
|
||||
query
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -85,6 +85,8 @@ export function toBase62(n: number): string {
|
|||
}
|
||||
|
||||
export function getConfig(): UserConfig {
|
||||
const localesHash = toBase62(hash(JSON.stringify(locales)));
|
||||
|
||||
return {
|
||||
base: '/vite/',
|
||||
|
||||
|
@ -188,9 +190,9 @@ export function getConfig(): UserConfig {
|
|||
// dependencies of i18n.ts
|
||||
'config': ['@@/js/config.js'],
|
||||
},
|
||||
entryFileNames: 'scripts/[hash:8].js',
|
||||
chunkFileNames: 'scripts/[hash:8].js',
|
||||
assetFileNames: 'assets/[hash:8][extname]',
|
||||
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
|
||||
paths(id) {
|
||||
for (const p of externalPackages) {
|
||||
if (p.match.test(id)) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.9.1-alpha.0",
|
||||
"version": "2025.9.1-alpha.1",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
106
pnpm-lock.yaml
106
pnpm-lock.yaml
|
@ -83,8 +83,8 @@ importers:
|
|||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
pnpm:
|
||||
specifier: 10.15.1
|
||||
version: 10.15.1
|
||||
specifier: 10.16.0
|
||||
version: 10.16.0
|
||||
start-server-and-test:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
|
@ -829,6 +829,9 @@ importers:
|
|||
matter-js:
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0
|
||||
mediabunny:
|
||||
specifier: 1.15.1
|
||||
version: 1.15.1
|
||||
mfm-js:
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
|
@ -2440,138 +2443,163 @@ packages:
|
|||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.2':
|
||||
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.5':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.2':
|
||||
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.2':
|
||||
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.2':
|
||||
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.33.5':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
|
@ -2893,30 +2921,35 @@ packages:
|
|||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
|
||||
resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
|
||||
resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
|
||||
resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.79':
|
||||
resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
|
||||
resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==}
|
||||
|
@ -3271,36 +3304,42 @@ packages:
|
|||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.0':
|
||||
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.0':
|
||||
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.0':
|
||||
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.0':
|
||||
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.0':
|
||||
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.0':
|
||||
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
|
||||
|
@ -3470,111 +3509,133 @@ packages:
|
|||
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.50.1':
|
||||
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
|
||||
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
||||
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.50.1':
|
||||
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
||||
|
@ -4308,24 +4369,28 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.13.5':
|
||||
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.13.5':
|
||||
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.13.5':
|
||||
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.13.5':
|
||||
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
|
||||
|
@ -4549,6 +4614,12 @@ packages:
|
|||
'@types/doctrine@0.0.9':
|
||||
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
|
||||
|
||||
'@types/dom-mediacapture-transform@0.1.11':
|
||||
resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==}
|
||||
|
||||
'@types/dom-webcodecs@0.1.13':
|
||||
resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==}
|
||||
|
||||
'@types/eslint@7.29.0':
|
||||
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
|
||||
|
||||
|
@ -8140,6 +8211,9 @@ packages:
|
|||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mediabunny@1.15.1:
|
||||
resolution: {integrity: sha512-+eRTVzd3E4LuGYZzPSQcPzuGdAIljohSlzYTX358XsfLM2qH1lQIBYa+erx7wzVcGQLRNjdV7x7ZS0EpK04DfA==}
|
||||
|
||||
meilisearch@0.52.0:
|
||||
resolution: {integrity: sha512-RqPsB4a78sXf/ATB7PIVvKCG7yf0y1M+uCj8Z9Wku44WmCy3iz0C1PHjVV5xphQolo09CdhdyFoRxHQSJkOdpg==}
|
||||
|
||||
|
@ -9010,8 +9084,8 @@ packages:
|
|||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pnpm@10.15.1:
|
||||
resolution: {integrity: sha512-NOU4wym1VTAUyo6PRTWZf5YYCh0PYUM5NXRJk1NQ2STiL4YUaCGRJk7DPRRirCFWGv+X9rsYBlNRwWLH6PbeZw==}
|
||||
pnpm@10.16.0:
|
||||
resolution: {integrity: sha512-gGbnsDQhe3AKmk27OgBQYdZBuhMKiZFSE6ELPKSRnBnAN77IBmr9xVm4ljX9uAaxbqZz8kaPuyiqv6E8U+P3aQ==}
|
||||
engines: {node: '>=18.12'}
|
||||
hasBin: true
|
||||
|
||||
|
@ -9914,24 +9988,28 @@ packages:
|
|||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
slacc-linux-arm64-musl@0.0.10:
|
||||
resolution: {integrity: sha512-3lUX7752f6Okn54aONioaA+9M5TvifqXBAart+u2lNXEdWmmh003cVSU2Vcwg7nJ9lLHtju2DkDmKKfJjFuShA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
slacc-linux-x64-gnu@0.0.10:
|
||||
resolution: {integrity: sha512-BxxvylF9zlOLRLCpiyMvKTIUpdLlpetNBJ+DSMDh5+Ggq+AmQz2NUGawmcBJw58F8nMCj9TpWLlGNWc2AuY+JQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
slacc-linux-x64-musl@0.0.10:
|
||||
resolution: {integrity: sha512-TYJi8LOtJiTFcZvka4du7bMjF9Bz1RHRwyLnScr5E5yjjgoLRrsvgSu7bxp87xH+rgJ3CdEwE3w3Ux8EiewHpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
slacc-win32-arm64-msvc@0.0.10:
|
||||
resolution: {integrity: sha512-1CHPLiDB4exzFyT5ndtJDsRRhBxNg8mGz6I6eJEMjelGkJR2KZPT9LZuby/1bS/bcVOr7zuJvGNfbEGBeHRwPQ==}
|
||||
|
@ -10959,6 +11037,9 @@ packages:
|
|||
vue-component-type-helpers@3.0.6:
|
||||
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
|
||||
|
||||
vue-component-type-helpers@3.1.0-alpha.0:
|
||||
resolution: {integrity: sha512-K1guwS1Oy0gNfBdIdIn8JMkUV+S38sriR1zf5dP+KkPS7/r5nHnPZUL74meY2CYlxYBH4qSQ+k7bpHfwiRvaMg==}
|
||||
|
||||
vue-demi@0.14.7:
|
||||
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -14869,7 +14950,7 @@ snapshots:
|
|||
storybook: 9.1.5(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(msw@2.11.1(@types/node@22.18.1)(typescript@5.9.2))(prettier@3.6.2)(utf-8-validate@6.0.5)(vite@7.1.5(@types/node@22.18.1)(sass@1.92.1)(terser@5.44.0)(tsx@4.20.5))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.0.6
|
||||
vue-component-type-helpers: 3.1.0-alpha.0
|
||||
|
||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.35.0)(typescript@5.9.2)':
|
||||
dependencies:
|
||||
|
@ -15221,6 +15302,12 @@ snapshots:
|
|||
|
||||
'@types/doctrine@0.0.9': {}
|
||||
|
||||
'@types/dom-mediacapture-transform@0.1.11':
|
||||
dependencies:
|
||||
'@types/dom-webcodecs': 0.1.13
|
||||
|
||||
'@types/dom-webcodecs@0.1.13': {}
|
||||
|
||||
'@types/eslint@7.29.0':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
@ -19668,6 +19755,11 @@ snapshots:
|
|||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
mediabunny@1.15.1:
|
||||
dependencies:
|
||||
'@types/dom-mediacapture-transform': 0.1.11
|
||||
'@types/dom-webcodecs': 0.1.13
|
||||
|
||||
meilisearch@0.52.0: {}
|
||||
|
||||
memoizerific@1.11.3:
|
||||
|
@ -20637,7 +20729,7 @@ snapshots:
|
|||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pnpm@10.15.1: {}
|
||||
pnpm@10.16.0: {}
|
||||
|
||||
polished@4.2.2:
|
||||
dependencies:
|
||||
|
@ -22745,6 +22837,8 @@ snapshots:
|
|||
|
||||
vue-component-type-helpers@3.0.6: {}
|
||||
|
||||
vue-component-type-helpers@3.1.0-alpha.0: {}
|
||||
|
||||
vue-demi@0.14.7(vue@3.5.21(typescript@5.9.2)):
|
||||
dependencies:
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
|
|
|
@ -30,3 +30,4 @@ onlyBuiltDependencies:
|
|||
- v-code-diff
|
||||
- vue-demi
|
||||
ignorePatchFailures: false
|
||||
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
|
||||
|
|
Loading…
Reference in New Issue