mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-22 09:24:22 +02:00
Merge remote-tracking branch 'msky/develop' into copilot/add-user-mute-settings
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
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';
|
||||
@@ -218,21 +217,15 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
/**
|
||||
* Path of configuration directory
|
||||
*/
|
||||
const dir = `${_dirname}/../../../.config`;
|
||||
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
|
||||
|
||||
/**
|
||||
* Path of configuration file
|
||||
*/
|
||||
export const path = process.env.MISSKEY_CONFIG_YML
|
||||
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
|
||||
: process.env.NODE_ENV === 'test'
|
||||
? resolve(dir, 'test.yml')
|
||||
: resolve(dir, 'default.yml');
|
||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
|
||||
|
||||
export function loadConfig(): Config {
|
||||
if (!fs.existsSync(compiledConfigFilePath)) {
|
||||
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
|
||||
}
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||
@@ -244,7 +237,7 @@ export function loadConfig(): Config {
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/boot.ts': { file: null } };
|
||||
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||
|
||||
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||
const version = meta.version;
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos
|
||||
import { bindThis } from '@/decorators.js';
|
||||
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 type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
@@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
|
||||
if ((followersOnlyBefore != null)
|
||||
&& (
|
||||
(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
|
||||
|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
|
||||
)
|
||||
) {
|
||||
if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) {
|
||||
packedNote.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
@@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
if (!hide) {
|
||||
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
|
||||
if ((hiddenBefore != null)
|
||||
&& (
|
||||
(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
|
||||
|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
|
||||
)
|
||||
) {
|
||||
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/backend/src/misc/should-hide-note-by-time.ts
Normal file
29
packages/backend/src/misc/should-hide-note-by-time.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* ノートが指定された時間条件に基づいて非表示対象かどうかを判定する
|
||||
* @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない)
|
||||
* @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト)
|
||||
* @returns 非表示にすべき場合は true
|
||||
*/
|
||||
export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean {
|
||||
if (hiddenBefore == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime();
|
||||
|
||||
if (hiddenBefore <= 0) {
|
||||
// 負の値: 作成からの経過時間(秒)で判定
|
||||
const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
|
||||
const hideAfterSeconds = Math.abs(hiddenBefore);
|
||||
return elapsedSeconds >= hideAfterSeconds;
|
||||
} else {
|
||||
// 正の値: 絶対的なタイムスタンプ(秒)で判定
|
||||
const createdAtSeconds = createdAtTime / 1000;
|
||||
return createdAtSeconds <= hiddenBefore;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
let Sentry: typeof import('@sentry/node') | undefined;
|
||||
if (Sentry != null) {
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((mod) => {
|
||||
Sentry = mod;
|
||||
});
|
||||
|
||||
@@ -5,21 +5,20 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import type { MiPoll } from '@/models/Poll.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
@@ -43,6 +42,7 @@ export class ExportClipsProcessorService {
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
@@ -100,16 +100,16 @@ export class ExportClipsProcessorService {
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const clips = await this.clipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
const query = this.clipsRepository.createQueryBuilder('clip')
|
||||
.where('clip.userId = :userId', { userId: user.id })
|
||||
.orderBy('clip.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('clip.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
const clips = await query.getMany();
|
||||
|
||||
if (clips.length === 0) {
|
||||
job.updateProgress(100);
|
||||
@@ -124,7 +124,7 @@ export class ExportClipsProcessorService {
|
||||
const isFirst = exportedClipsCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
await this.processClipNotes(writer, clip.id);
|
||||
await this.processClipNotes(writer, clip.id, user.id);
|
||||
|
||||
await writer.write(']}');
|
||||
exportedClipsCount++;
|
||||
@@ -134,22 +134,25 @@ export class ExportClipsProcessorService {
|
||||
}
|
||||
}
|
||||
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: string): Promise<void> {
|
||||
let exportedClipNotesCount = 0;
|
||||
let cursor: MiClipNote['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const clipNotes = await this.clipNotesRepository.find({
|
||||
where: {
|
||||
clipId,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ['note', 'note.user'],
|
||||
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||
const query = this.clipNotesRepository.createQueryBuilder('clipNote')
|
||||
.leftJoinAndSelect('clipNote.note', 'note')
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.where('clipNote.clipId = :clipId', { clipId })
|
||||
.orderBy('clipNote.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('clipNote.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, { id: userId });
|
||||
|
||||
const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (clipNotes.length === 0) {
|
||||
break;
|
||||
@@ -158,6 +161,11 @@ export class ExportClipsProcessorService {
|
||||
cursor = clipNotes.at(-1)?.id ?? null;
|
||||
|
||||
for (const clipNote of clipNotes) {
|
||||
const noteCreatedAt = this.idService.parse(clipNote.note.id).date;
|
||||
if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let poll: MiPoll | undefined;
|
||||
if (clipNote.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js';
|
||||
@@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
@@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService {
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const favorites = await this.noteFavoritesRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ['note', 'note.user'],
|
||||
}) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
|
||||
const query = this.noteFavoritesRepository.createQueryBuilder('favorite')
|
||||
.leftJoinAndSelect('favorite.note', 'note')
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.where('favorite.userId = :userId', { userId: user.id })
|
||||
.orderBy('favorite.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('favorite.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, { id: user.id });
|
||||
|
||||
const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (favorites.length === 0) {
|
||||
job.updateProgress(100);
|
||||
@@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService {
|
||||
cursor = favorites.at(-1)?.id ?? null;
|
||||
|
||||
for (const favorite of favorites) {
|
||||
const noteCreatedAt = this.idService.parse(favorite.note.id).date;
|
||||
if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let poll: MiPoll | undefined;
|
||||
if (favorite.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id });
|
||||
|
||||
@@ -313,12 +313,16 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
if (ep.meta.limit) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
let limitActor: string | null;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
limitActor = getIpHash(request.ip);
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
limitActor = null;
|
||||
} else {
|
||||
limitActor = getIpHash(request.ip);
|
||||
}
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
@@ -330,7 +334,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
||||
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
||||
|
||||
if (factor > 0) {
|
||||
if (limitActor != null && factor > 0) {
|
||||
// Rate limit
|
||||
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
|
||||
if (rateLimit != null) {
|
||||
|
||||
@@ -89,17 +89,21 @@ export class SigninApiService {
|
||||
return { error };
|
||||
}
|
||||
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
|
||||
@@ -84,19 +84,23 @@ export class SigninWithPasskeyApiService {
|
||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||
};
|
||||
|
||||
try {
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
try {
|
||||
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
|
||||
// NOTE: 1 Sign-in require 2 API calls
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate Passkey Auth challenge with context
|
||||
|
||||
Reference in New Issue
Block a user