1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-14 05:15:35 +02:00

Merge branch 'develop' into room

This commit is contained in:
syuilo
2026-03-15 16:38:33 +09:00
111 changed files with 3292 additions and 2748 deletions

View File

@@ -41,17 +41,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.15.11",
"@swc/core-darwin-x64": "1.15.11",
"@swc/core-darwin-arm64": "1.15.18",
"@swc/core-darwin-x64": "1.15.18",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.15.11",
"@swc/core-linux-arm64-gnu": "1.15.11",
"@swc/core-linux-arm64-musl": "1.15.11",
"@swc/core-linux-x64-gnu": "1.15.11",
"@swc/core-linux-x64-musl": "1.15.11",
"@swc/core-win32-arm64-msvc": "1.15.11",
"@swc/core-win32-ia32-msvc": "1.15.11",
"@swc/core-win32-x64-msvc": "1.15.11",
"@swc/core-linux-arm-gnueabihf": "1.15.18",
"@swc/core-linux-arm64-gnu": "1.15.18",
"@swc/core-linux-arm64-musl": "1.15.18",
"@swc/core-linux-x64-gnu": "1.15.18",
"@swc/core-linux-x64-musl": "1.15.18",
"@swc/core-win32-arm64-msvc": "1.15.18",
"@swc/core-win32-ia32-msvc": "1.15.18",
"@swc/core-win32-x64-msvc": "1.15.18",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.1.0",
@@ -71,8 +71,8 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.990.0",
"@aws-sdk/lib-storage": "3.990.0",
"@aws-sdk/client-s3": "3.1000.0",
"@aws-sdk/lib-storage": "3.1000.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
@@ -80,21 +80,21 @@
"@fastify/http-proxy": "11.4.1",
"@fastify/multipart": "9.4.0",
"@fastify/static": "9.0.0",
"@kitajs/html": "4.2.12",
"@kitajs/html": "4.2.13",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
"@napi-rs/canvas": "0.1.92",
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13",
"@nestjs/testing": "11.1.13",
"@napi-rs/canvas": "0.1.95",
"@nestjs/common": "11.1.14",
"@nestjs/core": "11.1.14",
"@nestjs/testing": "11.1.14",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.38.0",
"@sentry/profiling-node": "10.38.0",
"@simplewebauthn/server": "13.2.2",
"@sentry/node": "10.40.0",
"@sentry/profiling-node": "10.40.0",
"@simplewebauthn/server": "13.2.3",
"@sinonjs/fake-timers": "15.1.0",
"@smithy/node-http-handler": "4.4.10",
"@smithy/node-http-handler": "4.4.12",
"@swc/cli": "0.8.0",
"@swc/core": "1.15.11",
"@swc/core": "1.15.18",
"@twemoji/parser": "16.0.0",
"accepts": "1.3.8",
"ajv": "8.18.0",
@@ -103,7 +103,7 @@
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.2",
"bullmq": "5.69.2",
"bullmq": "5.70.1",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@@ -112,7 +112,7 @@
"content-disposition": "1.0.1",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
"fastify": "5.7.4",
"fastify": "5.8.1",
"fastify-raw-body": "5.0.0",
"feed": "5.2.0",
"file-type": "21.3.0",
@@ -122,7 +122,7 @@
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
"ioredis": "5.9.3",
"ioredis": "5.10.0",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0",
"is-svg": "6.1.0",
@@ -145,7 +145,7 @@
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.5.0",
"pg": "8.18.0",
"pg": "8.19.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@@ -157,14 +157,14 @@
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.2",
"sanitize-html": "2.17.0",
"sanitize-html": "2.17.1",
"secure-json-parse": "4.1.0",
"semver": "7.7.4",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.31.0",
"systeminformation": "5.31.1",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@@ -178,8 +178,8 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.13",
"@sentry/vue": "10.38.0",
"@nestjs/platform-express": "11.1.14",
"@sentry/vue": "10.40.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@@ -193,11 +193,11 @@
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "24.10.13",
"@types/nodemailer": "7.0.9",
"@types/node": "24.11.0",
"@types/nodemailer": "7.0.11",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.16.0",
"@types/pg": "8.18.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
@@ -212,8 +212,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.11",
"cross-env": "10.1.0",
@@ -224,7 +224,7 @@
"jest": "29.7.0",
"jest-mock": "29.7.0",
"js-yaml": "4.1.1",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"pid-port": "2.0.1",
"simple-oauth2": "5.1.0",
"supertest": "7.2.2",

View File

@@ -129,6 +129,9 @@ export interface NoteEventTypes {
type NoteStreamEventTypes = {
[key in keyof NoteEventTypes]: {
id: MiNote['id'];
userId: MiNote['userId'];
visibility: MiNote['visibility'];
visibleUserIds: MiNote['visibleUserIds'];
body: NoteEventTypes[key];
};
};
@@ -378,9 +381,12 @@ export class GlobalEventService {
}
@bindThis
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
public publishNoteStream<K extends keyof NoteEventTypes>(note: MiNote, type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${note.id}`, type, {
id: note.id,
userId: note.userId,
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
body: value,
});
}

View File

@@ -68,7 +68,7 @@ export class NoteDeleteService {
}
if (!quiet) {
this.globalEventService.publishNoteStream(note.id, 'deleted', {
this.globalEventService.publishNoteStream(note, 'deleted', {
deletedAt: deletedAt,
});

View File

@@ -83,7 +83,7 @@ export class PollService {
const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note, 'pollVoted', {
choice: choice,
userId: user.id,
});

View File

@@ -259,7 +259,7 @@ export class QueryService {
@bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
// This code must always be synchronized with the checks in NoteEntityService.isVisibleForMe and Stream abstract class Channel.isNoteVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => {
qb

View File

@@ -244,7 +244,7 @@ export class ReactionService {
},
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
this.globalEventService.publishNoteStream(note, 'reacted', {
reaction: decodedReaction.reaction,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
@@ -318,7 +318,7 @@ export class ReactionService {
.execute();
}
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
this.globalEventService.publishNoteStream(note, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id,
});

View File

@@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { CacheService } from '@/core/CacheService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
@@ -66,6 +67,7 @@ export class NoteEntityService implements OnModuleInit {
private reactionService: ReactionService;
private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService;
private cacheService: CacheService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor(
@@ -101,6 +103,7 @@ export class NoteEntityService implements OnModuleInit {
//private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
//private cacheService: CacheService,
) {
}
@@ -111,6 +114,7 @@ export class NoteEntityService implements OnModuleInit {
this.reactionService = this.moduleRef.get('ReactionService');
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService');
this.cacheService = this.moduleRef.get('CacheService');
}
@bindThis
@@ -125,75 +129,65 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
public async shouldHideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<boolean> {
if (meId === packedNote.userId) return false;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
return true;
}
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
return true;
}
// visibility が specified かつ自分が指定されていなかったら非表示
if (!hide) {
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (packedNote.visibility === 'specified') {
if (meId == null) {
return true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (!specified) {
hide = true;
}
if (!specified) {
return true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (!hide) {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,
},
});
hide = !isFollowing;
if (packedNote.visibility === 'followers') {
if (meId == null) {
return true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
return false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
return false;
} else {
// フォロワーかどうか
const followings = await this.cacheService.userFollowingsCache.fetch(meId);
if (!Object.hasOwn(followings, packedNote.userId)) {
return true;
}
}
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
return false;
}
@bindThis
public hideNote(packedNote: Packed<'Note'>): void {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
@bindThis
@@ -278,7 +272,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
@@ -468,8 +462,8 @@ export class NoteEntityService implements OnModuleInit {
this.treatVisibility(packed);
if (!opts.skipHide) {
await this.hideNote(packed, meId);
if (!opts.skipHide && await this.shouldHideNote(packed, meId)) {
this.hideNote(packed);
}
return packed;

View File

@@ -30,9 +30,9 @@ import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@@ -131,6 +131,7 @@ export class ActivityPubServerService {
if (signature.params.headers.indexOf('digest') === -1) {
// Digest not found.
reply.code(401);
return;
} else {
const digest = request.headers.digest;

View File

@@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
import { ReversiChannel } from './api/stream/channels/reversi.js';
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
@@ -98,6 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
QueueStatsChannel,
ServerStatsChannel,
UserListChannel,
NoteStreamingHidingService,
OpenApiServerService,
OAuth2ProviderService,
],

View File

@@ -391,7 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js';
export * as 'users/get-following-users-by-birthday' from './endpoints/users/get-following-users-by-birthday.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
export * as 'users/lists/create' from './endpoints/users/lists/create.js';

View File

@@ -341,7 +341,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.clientOptions !== undefined) {
set.clientOptions = {
...serverSettings.clientOptions,
...this.serverSettings.clientOptions,
...ps.clientOptions,
};
}

View File

@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const userExist = await this.usersRepository.exists({ where: { id: me.id } });
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file === null) throw new ApiError(meta.errors.noSuchFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));

View File

@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View File

@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View File

@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View File

@@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View File

@@ -155,7 +155,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const index = ps.choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note, 'pollVoted', {
choice: ps.choice,
userId: me.id,
});

View File

@@ -86,7 +86,7 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-birthday-users instead.' },
birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-users-by-birthday instead.' },
},
},
],
@@ -146,7 +146,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee');
// @deprecated use get-following-birthday-users instead.
// @deprecated use get-following-users-by-birthday instead.
if (ps.birthday) {
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');

View File

@@ -20,7 +20,7 @@ export const meta = {
requireCredential: true,
kind: 'read:account',
description: 'Find users who have a birthday on the specified range.',
description: 'Retrieve users who have a birthday on the specified range.',
res: {
type: 'array',

View File

@@ -1,60 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { HybridTimelineChannel } from './channels/hybrid-timeline.js';
import { LocalTimelineChannel } from './channels/local-timeline.js';
import { HomeTimelineChannel } from './channels/home-timeline.js';
import { GlobalTimelineChannel } from './channels/global-timeline.js';
import { MainChannel } from './channels/main.js';
import { ChannelChannel } from './channels/channel.js';
import { AdminChannel } from './channels/admin.js';
import { ServerStatsChannel } from './channels/server-stats.js';
import { QueueStatsChannel } from './channels/queue-stats.js';
import { UserListChannel } from './channels/user-list.js';
import { AntennaChannel } from './channels/antenna.js';
import { DriveChannel } from './channels/drive.js';
import { HashtagChannel } from './channels/hashtag.js';
import { RoleTimelineChannel } from './channels/role-timeline.js';
import { ChatUserChannel } from './channels/chat-user.js';
import { ChatRoomChannel } from './channels/chat-room.js';
import { ReversiChannel } from './channels/reversi.js';
import { ReversiGameChannel } from './channels/reversi-game.js';
import type { ChannelConstructor } from './channel.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ChannelsService {
constructor(
) {
}
@bindThis
public getChannelConstructor(name: string): ChannelConstructor<boolean> {
switch (name) {
case 'main': return MainChannel;
case 'homeTimeline': return HomeTimelineChannel;
case 'localTimeline': return LocalTimelineChannel;
case 'hybridTimeline': return HybridTimelineChannel;
case 'globalTimeline': return GlobalTimelineChannel;
case 'userList': return UserListChannel;
case 'hashtag': return HashtagChannel;
case 'roleTimeline': return RoleTimelineChannel;
case 'antenna': return AntennaChannel;
case 'channel': return ChannelChannel;
case 'drive': return DriveChannel;
case 'serverStats': return ServerStatsChannel;
case 'queueStats': return QueueStatsChannel;
case 'admin': return AdminChannel;
case 'chatUser': return ChatUserChannel;
case 'chatRoom': return ChatRoomChannel;
case 'reversi': return ReversiChannel;
case 'reversiGame': return ReversiGameChannel;
default:
throw new Error(`no such channel: ${name}`);
}
}
}

View File

@@ -4,23 +4,19 @@
*/
import * as WebSocket from 'ws';
import type { MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
import type { ChannelConstructor } from './channel.js';
import type { ChannelRequest } from './channel.js';
import { ContextIdFactory, ModuleRef, REQUEST } from '@nestjs/core';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js';
import { CacheService } from '@/core/CacheService.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import type { MiUser } from '@/models/User.js';
import { MainChannel } from '@/server/api/stream/channels/main.js';
import { HomeTimelineChannel } from '@/server/api/stream/channels/home-timeline.js';
import { LocalTimelineChannel } from '@/server/api/stream/channels/local-timeline.js';
@@ -39,20 +35,24 @@ import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
import type { ChannelRequest } from './channel.js';
import type { ChannelConstructor } from './channel.js';
import type Channel from './channel.js';
import type { EventEmitter } from 'events';
const MAX_CHANNELS_PER_CONNECTION = 32;
/**
* Main stream connection
*/
// eslint-disable-next-line import/no-default-export
@Injectable({ scope: Scope.TRANSIENT })
export default class Connection {
public user?: MiUser;
public token?: MiAccessToken;
private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private channels: Map<string, Channel> = new Map();
private subscribingNotes: Partial<Record<string, number>> = {};
public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@@ -206,6 +206,19 @@ export default class Connection {
@bindThis
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
// 自分自身ではないかつ
if (data.body.userId !== this.user?.id) {
// 公開範囲が指名で自分が含まれてない
if (data.body.visibility === 'specified' && (this.user == null || !data.body.visibleUserIds.includes(this.user.id))) {
return;
}
// 公開範囲がフォロワーで自分がフォロワーでない
if (data.body.visibility === 'followers' && !Object.hasOwn(this.following, data.body.userId)) {
return;
}
}
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,
@@ -254,7 +267,11 @@ export default class Connection {
*/
@bindThis
public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
if (this.channels.has(id)) {
this.disconnectChannel(id);
}
if (this.channels.size >= MAX_CHANNELS_PER_CONNECTION) {
return;
}
@@ -270,8 +287,12 @@ export default class Connection {
}
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
if (channelConstructor.shouldShare && this.channels.some(c => c.chName === channel)) {
return;
if (channelConstructor.shouldShare) {
for (const c of this.channels.values()) {
if (c.chName === channel) {
return;
}
}
}
const contextId = ContextIdFactory.create();
@@ -281,8 +302,13 @@ export default class Connection {
}, contextId);
const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId);
this.channels.push(ch);
ch.init(params ?? {});
this.channels.set(ch.id, ch);
const valid = await ch.init(params ?? {});
if (typeof valid === 'boolean' && !valid) {
// 初期化処理の結果、接続拒否されたので切断
this.disconnectChannel(id);
return;
}
if (pong) {
this.sendMessageToWs('connected', {
@@ -324,11 +350,11 @@ export default class Connection {
*/
@bindThis
public disconnectChannel(id: string) {
const channel = this.channels.find(c => c.id === id);
const channel = this.channels.get(id);
if (channel) {
if (channel.dispose) channel.dispose();
this.channels = this.channels.filter(c => c.id !== id);
this.channels.delete(id);
}
}
@@ -343,7 +369,7 @@ export default class Connection {
if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return;
const channel = this.channels.find(c => c.id === data.id);
const channel = this.channels.get(data.id);
if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body);
}
@@ -355,7 +381,7 @@ export default class Connection {
@bindThis
public dispose() {
if (this.fetchIntervalId) clearInterval(this.fetchIntervalId);
for (const c of this.channels.filter(c => c.dispose)) {
for (const c of this.channels.values()) {
if (c.dispose) c.dispose();
}
}

View File

@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiUser } from '@/models/User.js';
type HiddenLayer = 'note' | 'renote' | 'renoteRenote';
type LockdownCheckResult =
| { shouldSkip: true }
| { shouldSkip: false; hiddenLayers: Set<HiddenLayer> };
/** Streamにおいて、ートを隠すhideNoteを適用するためのService */
@Injectable()
export class NoteStreamingHidingService {
constructor(
private noteEntityService: NoteEntityService,
) {}
/**
* ノートの可視性を判定する
*
* @param note - 判定対象のノート
* @param meId - 閲覧者のユーザーID未ログインの場合はnull
* @returns shouldSkip: true の場合はートを流さない、false の場合は hiddenLayers に基づいて隠す
*/
@bindThis
public async shouldHide(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<LockdownCheckResult> {
const hiddenLayers = new Set<HiddenLayer>();
// 1階層目: note自体
const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId);
if (shouldHideThisNote) {
if (isRenotePacked(note) && isQuotePacked(note)) {
// 引用リノートの場合、内容を隠して流す
hiddenLayers.add('note');
} else if (isRenotePacked(note)) {
// 純粋リノートの場合、流さない
return { shouldSkip: true };
} else {
// 通常ノートの場合、内容を隠して流す
hiddenLayers.add('note');
}
}
// 2階層目: note.renote
if (isRenotePacked(note) && note.renote) {
const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId);
if (shouldHideRenote) {
if (isQuotePacked(note)) {
// noteが引用リートの場合、renote部分だけ隠す
hiddenLayers.add('renote');
} else {
// noteが純粋リートの場合、流さない
return { shouldSkip: true };
}
}
}
// 3階層目: note.renote.renote
if (isRenotePacked(note) && note.renote &&
isRenotePacked(note.renote) && note.renote.renote) {
const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId);
if (shouldHideRenoteRenote) {
if (isQuotePacked(note.renote)) {
// note.renoteが引用リートの場合、renote.renote部分だけ隠す
hiddenLayers.add('renoteRenote');
} else {
// note.renoteが純粋リートの場合、note.renoteの意味がなくなるので流さない
return { shouldSkip: true };
}
}
}
return { shouldSkip: false, hiddenLayers };
}
/**
* hiddenLayersに基づいてートの内容を隠す。
*
* この処理は渡された `note` を直接変更します。
*
* @param note - 処理対象のノート
* @param hiddenLayers - 隠す階層のセット
*/
@bindThis
public applyHiding(
note: Packed<'Note'>,
hiddenLayers: Set<HiddenLayer>,
): void {
if (hiddenLayers.has('note')) {
this.noteEntityService.hideNote(note);
}
if (hiddenLayers.has('renote') && note.renote) {
this.noteEntityService.hideNote(note.renote);
}
if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) {
this.noteEntityService.hideNote(note.renote.renote);
}
}
/**
* ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。
*
* この処理は渡された `note` を直接変更します。
*
* @param note - 処理対象のノート(必要に応じて内容が隠される)
* @param meId - 閲覧者のユーザーID未ログインの場合はnull
* @returns shouldSkip: true の場合はノートを流さない
*/
@bindThis
public async processHiding(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<{ shouldSkip: boolean }> {
const result = await this.shouldHide(note, meId);
if (result.shouldSkip) {
return { shouldSkip: true };
}
this.applyHiding(note, result.hiddenLayers);
return { shouldSkip: false };
}
}

View File

@@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import { isChannelRelated } from '@/misc/is-channel-related.js';
import type { Awaitable } from '@/types.js';
import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js';
@@ -64,6 +65,43 @@ export default abstract class Channel {
return this.connection.subscriber;
}
protected isNoteVisibleForMe(note: Packed<'Note'>): boolean {
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
const meId = this.connection.user?.id ?? null;
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
return note.visibleUserIds?.some(id => meId === id) ?? false;
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
return Object.hasOwn(this.following, note.userId);
}
}
return true;
}
/*
* ミュートとブロックされてるを処理する
*/
@@ -104,7 +142,14 @@ export default abstract class Channel {
});
}
public abstract init(params: JsonObject): void;
/**
* チャンネルの初期化処理(接続時点での接続可否チェックを兼ねる)
*
* - `void / Promise<void>` を返す場合は、チェックなし
* - `true / Promise<true>` を返す場合は、接続可能
* - `false / Promise<false>` を返す場合は、接続不可(接続を切断)
*/
public abstract init(params: JsonObject): Awaitable<void | boolean>;
public dispose?(): void;

View File

@@ -4,8 +4,12 @@
*/
import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennasRepository } from '@/models/_.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@@ -23,19 +27,36 @@ export class AntennaChannel extends Channel {
@Inject(REQUEST)
request: ChannelRequest,
@Inject(DI.antennasRepository)
private antennasReposiotry: AntennasRepository,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onEvent = this.onEvent.bind(this);
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.antennaId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.antennaId !== 'string') return false;
if (!this.user) return false;
this.antennaId = params.antennaId;
const antennaExists = await this.antennasReposiotry.exists({
where: {
id: this.antennaId,
userId: this.user.id,
},
});
if (!antennaExists) return false;
// Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
return true;
}
@bindThis
@@ -43,8 +64,21 @@ export class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View File

@@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@@ -26,6 +27,7 @@ export class ChannelChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -48,12 +50,18 @@ export class ChannelChannel extends Channel {
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -4,12 +4,14 @@
*/
import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { ChatService } from '@/core/ChatService.js';
import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core';
import type { ChatRoomsRepository } from '@/models/_.js';
@Injectable({ scope: Scope.TRANSIENT })
export class ChatRoomChannel extends Channel {
@@ -23,17 +25,31 @@ export class ChatRoomChannel extends Channel {
@Inject(REQUEST)
request: ChannelRequest,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
private chatService: ChatService,
) {
super(request);
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.roomId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.roomId !== 'string') return false;
if (!this.user) return false;
this.roomId = params.roomId;
const room = await this.chatRoomsRepository.findOneBy({
id: this.roomId,
});
if (room == null) return false;
if (!(await this.chatService.hasPermissionToViewRoomTimeline(this.user.id, room))) return false;
this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent);
return true;
}
@bindThis

View File

@@ -29,11 +29,16 @@ export class ChatUserChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.otherId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.otherId !== 'string') return false;
if (!this.user) return false;
if (params.otherId === this.user.id) return false;
this.otherId = params.otherId;
this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
this.subscriber.on(`chatUserStream:${this.user.id}-${this.otherId}`, this.onEvent);
return true;
}
@bindThis

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -29,6 +30,7 @@ export class GlobalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -60,10 +62,15 @@ export class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -7,12 +7,12 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class HashtagChannel extends Channel {
public readonly chName = 'hashtag';
@@ -25,19 +25,26 @@ export class HashtagChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: JsonObject) {
if (!Array.isArray(params.q)) return;
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
public async init(params: JsonObject): Promise<boolean> {
if (!Array.isArray(params.q)) return false;
if (!params.q.every((x): x is string[] => (
Array.isArray(x) &&
x.length >= 1 &&
x.every(y => typeof y === 'string')
))) return false;
this.q = params.q;
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
return true;
}
@bindThis
@@ -46,12 +53,21 @@ export class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
if (!this.isNoteVisibleForMe(note)) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
@@ -26,6 +27,7 @@ export class HomeTimelineChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -55,11 +57,7 @@ export class HomeTimelineChannel extends Channel {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -84,10 +82,15 @@ export class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -31,6 +32,7 @@ export class HybridTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -75,12 +77,7 @@ export class HybridTimelineChannel extends Channel {
}
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
@@ -104,10 +101,15 @@ export class HybridTimelineChannel extends Channel {
}
}
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
@@ -30,6 +31,7 @@ export class LocalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -70,10 +72,15 @@ export class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -28,9 +28,10 @@ export class MainChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
public async init(params: JsonObject): Promise<boolean> {
if (!this.user) return false;
this.subscriber.on(`mainStream:${this.user.id}`, async data => {
switch (data.type) {
case 'notification': {
// Ignore notifications from instances the user has muted
@@ -47,8 +48,8 @@ export class MainChannel extends Channel {
}
case 'mention': {
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (this.userIdsWhoMeMuting.has(data.body.userId)) return;
if (!this.isNoteVisibleForMe(data.body)) return;
if (this.isNoteMutedOrBlocked(data.body)) return;
if (data.body.isHidden) {
const note = await this.noteEntityService.pack(data.body.id, this.user, {
detail: true,
@@ -61,5 +62,7 @@ export class MainChannel extends Channel {
this.send(data.type, data.body);
});
return true;
}
}

View File

@@ -7,6 +7,8 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@@ -25,6 +27,7 @@ export class RoleTimelineChannel extends Channel {
private noteEntityService: NoteEntityService,
private roleservice: RoleService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@@ -47,9 +50,24 @@ export class RoleTimelineChannel extends Channel {
return;
}
if (note.visibility !== 'public') return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@@ -36,6 +37,7 @@ export class UserListChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.updateListUsers = this.updateListUsers.bind(this);
@@ -43,8 +45,8 @@ export class UserListChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.listId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.listId !== 'string') return false;
this.listId = params.listId;
this.withFiles = !!(params.withFiles ?? false);
this.withRenotes = !!(params.withRenotes ?? true);
@@ -56,7 +58,7 @@ export class UserListChannel extends Channel {
userId: this.user!.id,
},
});
if (!listExist) return;
if (!listExist) return false;
// Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send);
@@ -65,6 +67,8 @@ export class UserListChannel extends Channel {
this.updateListUsers();
this.listUsersClock = setInterval(this.updateListUsers, 5000);
return true;
}
@bindThis
@@ -96,11 +100,7 @@ export class UserListChannel extends Channel {
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -117,10 +117,15 @@ export class UserListChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View File

@@ -142,7 +142,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
}
if (res.headers.get('content-type')?.includes('application/json')) {
const contentType = res.headers.get('content-type');
const mediaType = contentType ? contentType.split(';')[0].trim() : null;
if (mediaType === 'application/json') {
// Client discovery via JSON document (11 July 2024 spec)
// https://indieauth.spec.indieweb.org/#client-metadata
// "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing

View File

@@ -414,3 +414,5 @@ export type FilterUnionByProperty<
Property extends string | number | symbol,
Condition,
> = Union extends Record<Property, Condition> ? Union : never;
export type Awaitable<T> = T | Promise<T>;

View File

@@ -1,6 +1,6 @@
services:
nginx:
image: nginx:1.27
image: nginx:1.29
volumes:
- type: bind
source: ./certificates/rootCA.crt

View File

@@ -11,10 +11,10 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"rollup": "4.57.1"
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"rollup": "4.59.0"
},
"dependencies": {
"i18n": "workspace:*",

View File

@@ -25,13 +25,13 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.57.1",
"rollup": "4.59.0",
"sass": "1.97.3",
"shiki": "3.22.0",
"shiki": "3.23.0",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
"vite": "7.3.1",
"vue": "3.5.28"
"vue": "3.5.29"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@@ -39,29 +39,29 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.10",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"@vue/runtime-core": "3.5.28",
"acorn": "8.15.0",
"@vue/runtime-core": "3.5.29",
"acorn": "8.16.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"happy-dom": "20.6.1",
"happy-dom": "20.7.0",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.12.10",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"prettier": "3.8.1",
"start-server-and-test": "2.1.3",
"start-server-and-test": "2.1.5",
"tsx": "4.21.0",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.2.4",
"vue-component-type-helpers": "3.2.5",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.4"
"vue-tsc": "3.2.5"
}
}

View File

@@ -21,12 +21,12 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"eslint-plugin-vue": "10.8.0",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"vue-eslint-parser": "10.4.0"
},
"files": [
@@ -35,6 +35,6 @@
"dependencies": {
"i18n": "workspace:*",
"misskey-js": "workspace:*",
"vue": "3.5.28"
"vue": "3.5.29"
}
}

View File

@@ -28,7 +28,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.38.0",
"@sentry/vue": "10.40.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@@ -43,13 +43,13 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "15.1.0",
"chromatic": "15.2.0",
"compare-versions": "6.1.1",
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
"eventemitter3": "5.0.4",
"execa": "9.6.1",
"exifreader": "4.36.1",
"exifreader": "4.36.2",
"frontend-shared": "workspace:*",
"i18n": "workspace:*",
"icons-subsetter": "workspace:*",
@@ -59,7 +59,7 @@
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
"mediabunny": "1.34.2",
"mediabunny": "1.35.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -68,38 +68,38 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.57.1",
"sanitize-html": "2.17.0",
"rollup": "4.59.0",
"sanitize-html": "2.17.1",
"sass": "1.97.3",
"shiki": "3.22.0",
"shiki": "3.23.0",
"textarea-caret": "3.1.0",
"three": "0.182.0",
"three": "0.183.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
"vite": "7.3.1",
"vue": "3.5.28",
"vue": "3.5.29",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
"@storybook/addon-essentials": "8.6.15",
"@storybook/addon-interactions": "8.6.15",
"@storybook/addon-links": "10.2.8",
"@storybook/addon-mdx-gfm": "8.6.15",
"@storybook/addon-storysource": "8.6.15",
"@storybook/blocks": "8.6.15",
"@storybook/components": "8.6.15",
"@storybook/core-events": "8.6.15",
"@storybook/manager-api": "8.6.15",
"@storybook/preview-api": "8.6.15",
"@storybook/react": "10.2.8",
"@storybook/react-vite": "10.2.8",
"@storybook/test": "8.6.15",
"@storybook/theming": "8.6.15",
"@storybook/types": "8.6.15",
"@storybook/vue3": "10.2.8",
"@storybook/vue3-vite": "10.2.8",
"@storybook/addon-essentials": "8.6.17",
"@storybook/addon-interactions": "8.6.17",
"@storybook/addon-links": "10.2.13",
"@storybook/addon-mdx-gfm": "8.6.17",
"@storybook/addon-storysource": "8.6.17",
"@storybook/blocks": "8.6.17",
"@storybook/components": "8.6.17",
"@storybook/core-events": "8.6.17",
"@storybook/manager-api": "8.6.17",
"@storybook/preview-api": "8.6.17",
"@storybook/react": "10.2.13",
"@storybook/react-vite": "10.2.13",
"@storybook/test": "8.6.17",
"@storybook/theming": "8.6.17",
"@storybook/types": "8.6.17",
"@storybook/vue3": "10.2.13",
"@storybook/vue3-vite": "10.2.13",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@@ -107,46 +107,46 @@
"@types/insert-text-at-cursor": "0.3.2",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"@vue/compiler-core": "3.5.28",
"acorn": "8.15.0",
"@vue/compiler-core": "3.5.29",
"acorn": "8.16.0",
"astring": "1.9.0",
"cross-env": "10.1.0",
"cypress": "15.10.0",
"cypress": "15.11.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"estree-walker": "3.0.3",
"happy-dom": "20.6.1",
"happy-dom": "20.7.0",
"intersection-observer": "0.12.2",
"magic-string": "0.30.21",
"micromatch": "4.0.8",
"minimatch": "10.2.2",
"minimatch": "10.2.4",
"msw": "2.12.10",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"prettier": "3.8.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.3",
"storybook": "10.2.8",
"start-server-and-test": "2.1.5",
"storybook": "10.2.13",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0",
"vite-plugin-glsl": "1.5.5",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.0.18",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.2.4",
"vue-component-type-helpers": "3.2.5",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.4"
"vue-tsc": "3.2.5"
}
}

View File

@@ -109,6 +109,7 @@ function onDragstart(ev: DragEvent, item: T) {
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
// SEE: https://issues.chromium.org/issues/41150279
window.setTimeout(() => {
dragging.value = true;
}, 10);

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
<div v-if="Object.values(form).filter(item => typeof item.hidden !== 'boolean' || item.hidden === true).length > 0" class="_gaps_m">
<template v-for="v, k in form">
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
<MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">

View File

@@ -233,7 +233,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
if (!useAnim) {
return genEl(token.children, scale);
}
return h(MkSparkle, {}, genEl(token.children, scale));
return h(MkSparkle, {}, { default: () => genEl(token.children, scale) });
}
case 'rotate': {
const degrees = safeParseFloat(token.props.args.deg) ?? 90;
@@ -363,7 +363,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
url: token.props.url,
rel: 'nofollow noopener',
navigationBehavior: props.linkNavigationBehavior,
}, genEl(token.children, scale, true))];
}, { default: () => genEl(token.children, scale, true) })];
}
case 'mention': {
@@ -381,7 +381,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--MI_THEME-hashtag);',
behavior: props.linkNavigationBehavior,
}, `#${token.props.hashtag}`)];
}, { default: () => `#${token.props.hashtag}` })];
}
case 'blockCode': {

View File

@@ -82,8 +82,10 @@ export class Pizzax<T extends StateDef> {
this.r = {} as ReactiveState<T>;
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
this.s[k] = v.default;
this.r[k] = ref(v.default);
// 参照渡しになるのを防ぐためclone
const defaultValue = deepClone(v.default);
this.s[k] = defaultValue;
this.r[k] = ref(defaultValue);
}
this.ready = this.init();
@@ -120,7 +122,8 @@ export class Pizzax<T extends StateDef> {
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
this.r[k].value = this.s[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else {
this.r[k].value = this.s[k] = v.default;
// 参照渡しになるのを防ぐためclone
this.r[k].value = this.s[k] = deepClone(v.default);
}
}
@@ -148,7 +151,8 @@ export class Pizzax<T extends StateDef> {
this.r[k].value = this.s[k] = (kvs as Partial<T>)[k];
cache[k] = (kvs as Partial<T>)[k];
} else {
this.r[k].value = this.s[k] = v.default;
// 参照渡しになるのを防ぐためclone
this.r[k].value = this.s[k] = deepClone(v.default);
}
}
}
@@ -218,8 +222,10 @@ export class Pizzax<T extends StateDef> {
}
public reset(key: keyof T) {
this.set(key, this.def[key].default);
return this.def[key].default;
// 参照渡しになるのを防ぐためclone
const defaultValue = deepClone(this.def[key].default);
this.set(key, defaultValue);
return defaultValue;
}
/**

View File

@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template #default="{ item, dragStart }">
<div :class="$style.item">
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
<!-- divが無いとエラーになる -->
<RolesEditorFormula
:modelValue="item"
:dragStartCallback="dragStart"

View File

@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template #default="{ item }">
<div>
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
<!-- divが無いとエラーになる -->
<component :is="getComponent(item.type) as any" :modelValue="item" @update:modelValue="updateItem" @remove="() => removeItem(item)"/>
</div>
</template>

View File

@@ -51,10 +51,12 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
import { enableStoragePersistence, storagePersisted, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const storagePersisted = await getStoragePersistenceStatusRef();
const indexInfo = {
title: i18n.ts.settings,
icon: 'ti ti-settings',

View File

@@ -165,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
import { enableStoragePersistence, storagePersisted, storagePersistenceSupported } from '@/utility/storage.js';
import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -180,6 +180,8 @@ import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin();
const storagePersisted = await getStoragePersistenceStatusRef();
const reportError = prefer.model('reportError');
const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');

View File

@@ -14,6 +14,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { deepEqual } from '@/utility/deep-equal.js';
import { deepClone } from '@/utility/clone.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@@ -122,7 +123,8 @@ export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
if (typeof _default === 'function') { // factory
return _default() as ValueOf<K>;
} else {
return _default as unknown as ValueOf<K>;
// 参照渡しになるのを防ぐためclone
return deepClone(_default as unknown as ValueOf<K>);
}
}

View File

@@ -10,6 +10,10 @@ const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
const float goldenAngle = 2.39996323;
const int sampleCount = 256;
const float sampleCountF = float(sampleCount);
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
@@ -18,7 +22,6 @@ uniform vec2 u_scale;
uniform bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
float rand(vec2 value) {
@@ -51,17 +54,7 @@ void main() {
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0;
float radiusPx = normalizedRadius * referenceSize;
vec2 texelSize = 1.0 / in_resolution;
int sampleCount = max(u_samples, 1);
float sampleCountF = float(sampleCount);
float jitter = rand(in_uv * in_resolution);
float goldenAngle = 2.39996323;
float jitter = rand(in_uv);
// Sample in a circular pattern to avoid axis-aligned artifacts
for (int i = 0; i < sampleCount; i++) {
@@ -69,15 +62,11 @@ void main() {
float radius = sqrt((fi + 0.5) / sampleCountF);
float theta = (fi + jitter) * goldenAngle;
vec2 direction = vec2(cos(theta), sin(theta));
vec2 offset = direction * (radiusPx * radius) * texelSize;
vec2 sampleUV = in_uv + offset;
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
float weight = exp(-radius * radius * 4.0);
result += texture(in_texture, sampleUV) * weight;
totalSamples += weight;
}
vec2 offset = direction * (u_radius * radius);
float weight = exp(-radius * radius * 4.0);
result += texture(in_texture, in_uv + offset) * weight;
totalSamples += weight;
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
out_color = result / totalSamples;
}

View File

@@ -24,7 +24,6 @@ export const fn = defineImageCompositorFunction<{
gl.uniform1i(u.ellipse, params.ellipse ? 1 : 0);
gl.uniform1f(u.angle, params.angle / 2);
gl.uniform1f(u.radius, params.radius);
gl.uniform1i(u.samples, 256);
},
});
@@ -84,10 +83,10 @@ export const uiDefinition = {
radius: {
label: i18n.ts._imageEffector._fxProps.strength,
type: 'number',
default: 10.0,
default: 0.15,
min: 0.0,
max: 20.0,
step: 0.5,
max: 0.3,
step: 0.01,
},
},
} satisfies ImageEffectorUiDefinition<typeof fn>;

View File

@@ -14,12 +14,15 @@ uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_pos;
uniform float u_frequency;
uniform bool u_thresholdEnabled;
uniform float u_threshold;
uniform float u_outlineThickness;
uniform float u_maskSize;
uniform bool u_black;
out vec4 out_color;
float remap(float value, float inputMin, float inputMax, float outputMin, float outputMax) {
return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin));
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5));
@@ -33,16 +36,19 @@ void main() {
float noiseY = (noiseUV.y + seed) * u_frequency;
float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0;
float t = noise;
if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0;
if (noise < u_threshold) {
out_color = in_color;
} else {
float n = remap(noise, u_threshold, 1.0, 0.0, 1.0);
// TODO: マスクの形自体も揺らぎを与える
float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0));
float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0)));
out_color = vec4(
mix(in_color.r, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.g, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.b, u_black ? 0.0 : 1.0, t * mask),
in_color.a
);
// TODO: マスクの形自体も揺らぎを与える
float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0));
float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0)));
out_color = vec4(
mix(in_color.r, n < u_outlineThickness ? 0.0 : 1.0, mask),
mix(in_color.g, n < u_outlineThickness ? 0.0 : 1.0, mask),
mix(in_color.b, n < u_outlineThickness ? 0.0 : 1.0, mask),
in_color.a
);
}
}

View File

@@ -12,20 +12,17 @@ export const fn = defineImageCompositorFunction<{
x: number;
y: number;
frequency: number;
smoothing: boolean;
threshold: number;
density: number;
outlineThickness: number;
maskSize: number;
black: boolean;
}>({
shader,
main: ({ gl, u, params }) => {
gl.uniform2f(u.pos, params.x / 2, params.y / 2);
gl.uniform1f(u.frequency, params.frequency * params.frequency);
// thresholdの調整が有効な間はsmoothingが利用できない
gl.uniform1i(u.thresholdEnabled, params.smoothing ? 0 : 1);
gl.uniform1f(u.threshold, params.threshold);
gl.uniform1f(u.threshold, 1.0 - params.density);
gl.uniform1f(u.outlineThickness, params.outlineThickness);
gl.uniform1f(u.maskSize, params.maskSize);
gl.uniform1i(u.black, params.black ? 1 : 0);
},
});
@@ -56,20 +53,22 @@ export const uiDefinition = {
max: 15.0,
step: 0.1,
},
smoothing: {
label: i18n.ts._imageEffector._fxProps.zoomLinesSmoothing,
caption: i18n.ts._imageEffector._fxProps.zoomLinesSmoothingDescription,
type: 'boolean',
default: false,
},
threshold: {
label: i18n.ts._imageEffector._fxProps.zoomLinesThreshold,
density: {
label: i18n.ts._imageEffector._fxProps.density,
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
},
outlineThickness: {
label: i18n.ts._imageEffector._fxProps.zoomLinesOutlineThickness,
type: 'number',
default: 0.25,
min: 0.0,
max: 1.0,
step: 0.01,
},
maskSize: {
label: i18n.ts._imageEffector._fxProps.zoomLinesMaskSize,
type: 'number',
@@ -78,10 +77,5 @@ export const uiDefinition = {
max: 1.0,
step: 0.01,
},
black: {
label: i18n.ts._imageEffector._fxProps.zoomLinesBlack,
type: 'boolean',
default: false,
},
},
} satisfies ImageEffectorUiDefinition<typeof fn>;

View File

@@ -3,13 +3,21 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
import { readonly, ref } from 'vue';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
export const storagePersistenceSupported = window.isSecureContext && 'storage' in navigator;
export const storagePersisted = ref(storagePersistenceSupported ? await navigator.storage.persisted() : false);
const storagePersisted = ref(false);
export async function getStoragePersistenceStatusRef() {
if (storagePersistenceSupported) {
storagePersisted.value = await navigator.storage.persisted().catch(() => false);
}
return readonly(storagePersisted);
}
export async function enableStoragePersistence() {
if (!storagePersistenceSupported) return;

View File

@@ -30,7 +30,7 @@ import { useLowresTime } from '@/composables/use-lowres-time.js';
import { userPage, acct } from '@/filters/user.js';
const props = defineProps<{
item: Misskey.entities.UsersGetFollowingBirthdayUsersResponse[number];
item: Misskey.entities.UsersGetFollowingUsersByBirthdayResponse[number];
}>();
const now = useLowresTime();

View File

@@ -106,7 +106,7 @@ const end = computed(() => {
}
});
const birthdayUsersPaginator = markRaw(new Paginator('users/get-following-birthday-users', {
const birthdayUsersPaginator = markRaw(new Paginator('users/get-following-users-by-birthday', {
limit: 18,
offsetMode: true,
computedParams: computed(() => {

View File

@@ -29,13 +29,13 @@
],
"devDependencies": {
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"chokidar": "5.0.0",
"esbuild": "0.27.3",
"execa": "9.6.1",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"tsx": "4.21.0"
},
"dependencies": {

View File

@@ -13109,25 +13109,17 @@ export interface Locale extends ILocale {
*/
"centerY": string;
/**
* スムージング
* 密度
*/
"zoomLinesSmoothing": string;
"density": string;
/**
* スムージングと集中線の幅の設定は併用できません。
* 線の影の太さ
*/
"zoomLinesSmoothingDescription": string;
/**
* 集中線の幅
*/
"zoomLinesThreshold": string;
"zoomLinesOutlineThickness": string;
/**
* 中心径
*/
"zoomLinesMaskSize": string;
/**
* 黒色にする
*/
"zoomLinesBlack": string;
/**
* 円形
*/

View File

@@ -11,14 +11,14 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0"
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",
"harfbuzzjs": "0.8.0",
"harfbuzzjs": "0.10.0",
"tsx": "4.21.0",
"wawoff2": "2.0.1"
},

View File

@@ -25,13 +25,13 @@
},
"devDependencies": {
"@types/matter-js": "0.20.2",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"execa": "9.6.1",
"nodemon": "3.1.11"
"nodemon": "3.1.14"
},
"files": [
"built"

View File

@@ -2128,8 +2128,8 @@ declare namespace entities {
UsersFollowingResponse,
UsersGalleryPostsRequest,
UsersGalleryPostsResponse,
UsersGetFollowingBirthdayUsersRequest,
UsersGetFollowingBirthdayUsersResponse,
UsersGetFollowingUsersByBirthdayRequest,
UsersGetFollowingUsersByBirthdayResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
UsersListsCreateRequest,
@@ -3741,10 +3741,10 @@ type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBo
type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
type UsersGetFollowingUsersByBirthdayRequest = operations['users___get-following-users-by-birthday']['requestBody']['content']['application/json'];
// @public (undocumented)
type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
type UsersGetFollowingUsersByBirthdayResponse = operations['users___get-following-users-by-birthday']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];

View File

@@ -8,14 +8,14 @@
},
"devDependencies": {
"@readme/openapi-parser": "5.5.0",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"openapi-types": "12.1.3",
"openapi-typescript": "7.13.0",
"ts-case-convert": "2.1.0",
"tsx": "4.21.0",
"eslint": "9.39.2"
"eslint": "9.39.3"
},
"files": [
"built"

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.2.0-beta.0",
"version": "2026.3.2-alpha.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -37,15 +37,15 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.56.3",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@microsoft/api-extractor": "7.57.6",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"esbuild": "0.27.3",
"execa": "9.6.1",
"ncp": "2.0.0",
"nodemon": "3.1.11",
"nodemon": "3.1.14",
"tsd": "0.33.0",
"vitest": "4.0.18",
"vitest-websocket-mock": "0.5.0"

View File

@@ -4533,11 +4533,11 @@ declare module '../api.js' {
): Promise<SwitchCaseResponseType<E, P>>;
/**
* Find users who have a birthday on the specified range.
* Retrieve users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'users/get-following-birthday-users', P extends Endpoints[E]['req']>(
request<E extends 'users/get-following-users-by-birthday', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,

View File

@@ -616,8 +616,8 @@ import type {
UsersFollowingResponse,
UsersGalleryPostsRequest,
UsersGalleryPostsResponse,
UsersGetFollowingBirthdayUsersRequest,
UsersGetFollowingBirthdayUsersResponse,
UsersGetFollowingUsersByBirthdayRequest,
UsersGetFollowingUsersByBirthdayResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
UsersListsCreateRequest,
@@ -1069,7 +1069,7 @@ export type Endpoints = {
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
'users/get-following-birthday-users': { req: UsersGetFollowingBirthdayUsersRequest; res: UsersGetFollowingBirthdayUsersResponse };
'users/get-following-users-by-birthday': { req: UsersGetFollowingUsersByBirthdayRequest; res: UsersGetFollowingUsersByBirthdayResponse };
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
'users/lists/create-from-public': { req: UsersListsCreateFromPublicRequest; res: UsersListsCreateFromPublicResponse };

View File

@@ -619,8 +619,8 @@ export type UsersFollowingRequest = operations['users___following']['requestBody
export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json'];
export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json'];
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
export type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
export type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
export type UsersGetFollowingUsersByBirthdayRequest = operations['users___get-following-users-by-birthday']['requestBody']['content']['application/json'];
export type UsersGetFollowingUsersByBirthdayResponse = operations['users___get-following-users-by-birthday']['responses']['200']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
export type UsersListsCreateRequest = operations['users___lists___create']['requestBody']['content']['application/json'];

View File

@@ -3717,14 +3717,14 @@ export type paths = {
*/
post: operations['users___gallery___posts'];
};
'/users/get-following-birthday-users': {
'/users/get-following-users-by-birthday': {
/**
* users/get-following-birthday-users
* @description Find users who have a birthday on the specified range.
* users/get-following-users-by-birthday
* @description Retrieve users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['users___get-following-birthday-users'];
post: operations['users___get-following-users-by-birthday'];
};
'/users/get-frequently-replied-users': {
/**
@@ -34882,7 +34882,7 @@ export interface operations {
untilDate?: number;
/** @default 10 */
limit?: number;
/** @description @deprecated use get-following-birthday-users instead. */
/** @description @deprecated use get-following-users-by-birthday instead. */
birthday?: string | null;
};
};
@@ -35018,7 +35018,7 @@ export interface operations {
};
};
};
'users___get-following-birthday-users': {
'users___get-following-users-by-birthday': {
requestBody: {
content: {
'application/json': {

View File

@@ -24,12 +24,12 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"execa": "9.6.1",
"nodemon": "3.1.11"
"nodemon": "3.1.14"
},
"files": [
"built"

View File

@@ -15,10 +15,10 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/parser": "8.56.1",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.11"
"nodemon": "3.1.14"
},
"type": "module"
}