diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-07 21:23:03 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-07 21:23:03 +0900 |
| commit | 84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b (patch) | |
| tree | a182502a5192992d873e7a7fcbf01662bb0dfca2 /packages/backend/src | |
| parent | Merge pull request #8821 from misskey-dev/develop (diff) | |
| parent | 12.112.1 (diff) | |
| download | misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.tar.gz misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.tar.bz2 misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/backend/src')
177 files changed, 1715 insertions, 643 deletions
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 948545db7a..78510c8377 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -19,6 +19,7 @@ export type Source = { redis: { host: string; port: number; + family?: number; pass: string; db?: number; prefix?: string; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 298f6713ea..94d55e4310 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -68,9 +68,10 @@ import { RegistryItem } from '@/models/entities/registry-item.js'; import { Ad } from '@/models/entities/ad.js'; import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; import { UserPending } from '@/models/entities/user-pending.js'; +import { Webhook } from '@/models/entities/webhook.js'; +import { UserIp } from '@/models/entities/user-ip.js'; import { entities as charts } from '@/services/chart/entities.js'; -import { Webhook } from '@/models/entities/webhook.js'; import { envOption } from '../env.js'; import { dbLogger } from './logger.js'; import { redisClient } from './redis.js'; @@ -173,6 +174,7 @@ export const entities = [ PasswordResetRequest, UserPending, Webhook, + UserIp, ...charts, ]; @@ -192,12 +194,13 @@ export const db = new DataSource({ synchronize: process.env.NODE_ENV === 'test', dropSchema: process.env.NODE_ENV === 'test', cache: !config.db.disableCache ? { - type: 'redis', + type: 'ioredis', options: { host: config.redis.host, port: config.redis.port, + family: config.redis.family == null ? 0 : config.redis.family, password: config.redis.pass, - prefix: `${config.redis.prefix}:query:`, + keyPrefix: `${config.redis.prefix}:query:`, db: config.redis.db || 0, }, } : false, @@ -226,7 +229,7 @@ export async function initDb(force = false) { export async function resetDb() { const reset = async () => { - await redisClient.FLUSHDB(); + await redisClient.flushdb(); const tables = await db.query(`SELECT relname AS "table" FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts index 9346041456..49f5bb2ba8 100644 --- a/packages/backend/src/db/redis.ts +++ b/packages/backend/src/db/redis.ts @@ -1,16 +1,15 @@ -import * as redis from 'redis'; +import Redis from 'ioredis'; import config from '@/config/index.js'; export function createConnection() { - return redis.createClient( - config.redis.port, - config.redis.host, - { - password: config.redis.pass, - prefix: config.redis.prefix, - db: config.redis.db || 0, - } - ); + return new Redis({ + port: config.redis.port, + host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, + password: config.redis.pass, + keyPrefix: `${config.redis.prefix}:`, + db: config.redis.db || 0, + }); } export const subsdcriber = createConnection(); diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts index 15110b6b70..7751bac563 100644 --- a/packages/backend/src/mfm/from-html.ts +++ b/packages/backend/src/mfm/from-html.ts @@ -1,8 +1,10 @@ -import * as parse5 from 'parse5'; -import treeAdapter from 'parse5/lib/tree-adapters/default.js'; import { URL } from 'node:url'; +import * as parse5 from 'parse5'; +import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; + +const treeAdapter = TreeAdapter.defaultTreeAdapter; -const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; +const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; export function fromHtml(html: string, hashtagNames?: string[]): string { @@ -19,7 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { return text.trim(); - function getText(node: parse5.Node): string { + function getText(node: TreeAdapter.Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; @@ -31,7 +33,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { return ''; } - function appendChildren(childNodes: parse5.ChildNode[]): void { + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); @@ -39,7 +41,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { } } - function analyze(node: parse5.Node) { + function analyze(node: TreeAdapter.Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; @@ -170,7 +172,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string { const t = getText(node); if (t) { text += '\n> '; - text += t.split('\n').join(`\n> `); + text += t.split('\n').join('\n> '); } break; } diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index 588dc79e55..d7662820af 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -16,11 +16,13 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi if (me && (note.userId === me.id)) return false; if (mutedWords.length > 0) { - if (note.text == null) return false; + const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); + + if (text === '') return false; const matched = mutedWords.some(filter => { if (Array.isArray(filter)) { - return filter.every(keyword => note.text!.includes(keyword)); + return filter.every(keyword => text.includes(keyword)); } else { // represents RegExp const regexp = filter.match(/^\/(.+)\/(.*)$/); @@ -29,7 +31,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi if (!regexp) return false; try { - return new RE2(regexp[1], regexp[2]).test(note.text!); + return new RE2(regexp[1], regexp[2]).test(text); } catch (err) { // This should never happen due to input sanitisation. return false; diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index f07be634fb..fa88769de0 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -11,9 +11,14 @@ export function createTemp(): Promise<[string, () => void]> { export function createTempDir(): Promise<[string, () => void]> { return new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); + tmp.dir( + { + unsafeCleanup: true, + }, + (e, path, cleanup) => { + if (e) return rej(e); + res([path, cleanup]); + } + ); }); } diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts index d70dc3d70c..42061fcf83 100644 --- a/packages/backend/src/misc/get-file-info.ts +++ b/packages/backend/src/misc/get-file-info.ts @@ -1,12 +1,18 @@ import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; +import { join } from 'node:path'; import * as stream from 'node:stream'; import * as util from 'node:util'; +import { FSWatcher } from 'chokidar'; import { fileTypeFromFile } from 'file-type'; +import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; +import { type predictionType } from 'nsfwjs'; import sharp from 'sharp'; import { encode } from 'blurhash'; +import { detectSensitive } from '@/services/detect-sensitive.js'; +import { createTempDir } from './create-temp.js'; const pipeline = util.promisify(stream.pipeline); @@ -21,6 +27,8 @@ export type FileInfo = { height?: number; orientation?: number; blurhash?: string; + sensitive: boolean; + porn: boolean; warnings: string[]; }; @@ -37,7 +45,12 @@ const TYPE_SVG = { /** * Get file information */ -export async function getFileInfo(path: string): Promise<FileInfo> { +export async function getFileInfo(path: string, opts: { + skipSensitiveDetection: boolean; + sensitiveThreshold?: number; + sensitiveThresholdForPorn?: number; + enableSensitiveMediaDetectionForVideos?: boolean; +}): Promise<FileInfo> { const warnings = [] as string[]; const size = await getFileSize(path); @@ -58,7 +71,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> { // うまく判定できない画像は octet-stream にする if (!imageSize) { - warnings.push(`cannot detect image dimensions`); + warnings.push('cannot detect image dimensions'); type = TYPE_OCTET_STREAM; } else if (imageSize.wUnits === 'px') { width = imageSize.width; @@ -67,7 +80,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> { // 制限を超えている画像は octet-stream にする if (imageSize.width > 16383 || imageSize.height > 16383) { - warnings.push(`image dimensions exceeds limits`); + warnings.push('image dimensions exceeds limits'); type = TYPE_OCTET_STREAM; } } else { @@ -84,6 +97,19 @@ export async function getFileInfo(path: string): Promise<FileInfo> { }); } + let sensitive = false; + let porn = false; + + if (!opts.skipSensitiveDetection) { + [sensitive, porn] = await detectSensitivity( + path, + type.mime, + opts.sensitiveThreshold ?? 0.5, + opts.sensitiveThresholdForPorn ?? 0.75, + opts.enableSensitiveMediaDetectionForVideos ?? false, + ); + } + return { size, md5, @@ -92,10 +118,150 @@ export async function getFileInfo(path: string): Promise<FileInfo> { height, orientation, blurhash, + sensitive, + porn, warnings, }; } +async function detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { + let sensitive = false; + let porn = false; + + function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { + let sensitive = false; + let porn = false; + + if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; + + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; + + return [sensitive, porn]; + } + + if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) { + const result = await detectSensitive(source); + if (result) { + [sensitive, porn] = judgePrediction(result); + } + } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { + const [outDir, disposeOutDir] = await createTempDir(); + try { + const command = FFmpeg() + .input(source) + .inputOptions([ + '-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) + '-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) + ]) + .noAudio() + .videoFilters([ + { + filter: 'select', // フレームのフィルタリング + options: { + e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい) + }, + }, + { + filter: 'blackframe', // 暗いフレームの検出 + options: { + amount: '0', // 暗さに関わらず全てのフレームで測定値を取る + }, + }, + { + filter: 'metadata', + options: { + mode: 'select', // フレーム選択モード + key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) + value: '50', + function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので) + }, + }, + { + filter: 'scale', + options: { + w: 299, + h: 299, + }, + }, + ]) + .format('image2') + .output(join(outDir, '%d.png')) + .outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない + const results: ReturnType<typeof judgePrediction>[] = []; + let frameIndex = 0; + let targetIndex = 0; + let nextIndex = 1; + for await (const path of asyncIterateFrames(outDir, command)) { + try { + const index = frameIndex++; + if (index !== targetIndex) { + continue; + } + targetIndex = nextIndex; + nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける + const result = await detectSensitive(path); + if (result) { + results.push(judgePrediction(result)); + } + } finally { + fs.promises.unlink(path); + } + } + sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); + porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); + } finally { + disposeOutDir(); + } + } + + return [sensitive, porn]; +} + +async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> { + const watcher = new FSWatcher({ + cwd, + disableGlobbing: true, + }); + let finished = false; + command.once('end', () => { + finished = true; + watcher.close(); + }); + command.run(); + for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition + const current = `${i}.png`; + const next = `${i + 1}.png`; + const framePath = join(cwd, current); + if (await exists(join(cwd, next))) { + yield framePath; + } else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition + watcher.add(next); + await new Promise<void>((resolve, reject) => { + watcher.on('add', function onAdd(path) { + if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている + watcher.unwatch(current); + watcher.off('add', onAdd); + resolve(); + } + }); + command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている + command.once('error', reject); + }); + yield framePath; + } else if (await exists(framePath)) { + yield framePath; + } else { + return; + } + } +} + +function exists(path: string): Promise<boolean> { + return fs.promises.access(path).then(() => true, () => false); +} + /** * Detect MIME Type and extension */ diff --git a/packages/backend/src/misc/is-blocker-user-related.ts b/packages/backend/src/misc/is-blocker-user-related.ts deleted file mode 100644 index 8c0ebfad9b..0000000000 --- a/packages/backend/src/misc/is-blocker-user-related.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean { - if (blockerUserIds.has(note.userId)) { - return true; - } - - if (note.reply != null && blockerUserIds.has(note.reply.userId)) { - return true; - } - - if (note.renote != null && blockerUserIds.has(note.renote.userId)) { - return true; - } - - return false; -} diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts new file mode 100644 index 0000000000..8993ede33a --- /dev/null +++ b/packages/backend/src/misc/is-mime-image.ts @@ -0,0 +1,8 @@ +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; + +const dictionary = { + 'safe-file': FILE_TYPE_BROWSERSAFE, + 'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'], +}; + +export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); diff --git a/packages/backend/src/misc/is-muted-user-related.ts b/packages/backend/src/misc/is-muted-user-related.ts deleted file mode 100644 index 2caa743f95..0000000000 --- a/packages/backend/src/misc/is-muted-user-related.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean { - if (mutedUserIds.has(note.userId)) { - return true; - } - - if (note.reply != null && mutedUserIds.has(note.reply.userId)) { - return true; - } - - if (note.renote != null && mutedUserIds.has(note.renote.userId)) { - return true; - } - - return false; -} diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts new file mode 100644 index 0000000000..e6bbdb5d35 --- /dev/null +++ b/packages/backend/src/misc/is-user-related.ts @@ -0,0 +1,15 @@ +export function isUserRelated(note: any, userIds: Set<string>): boolean { + if (userIds.has(note.userId)) { + return true; + } + + if (note.reply != null && userIds.has(note.reply.userId)) { + return true; + } + + if (note.renote != null && userIds.has(note.renote.userId)) { + return true; + } + + return false; +} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index a636d1d519..d410b1d429 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -1,7 +1,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; import { User } from './user.js'; import { DriveFolder } from './drive-folder.js'; -import { id } from '../id.js'; @Entity() @Index(['userId', 'folderId', 'id']) @@ -156,6 +156,19 @@ export class DriveFile { }) public isSensitive: boolean; + @Index() + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is NSFW. (predict)', + }) + public maybeSensitive: boolean; + + @Index() + @Column('boolean', { + default: false, + }) + public maybePorn: boolean; + /** * 外部の(信頼されていない)URLへの直リンクか否か */ @@ -165,4 +178,15 @@ export class DriveFile { comment: 'Whether the DriveFile is direct link to remote server.', }) public isLink: boolean; + + @Column('jsonb', { + default: {}, + nullable: true, + }) + public requestHeaders: Record<string, string> | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public requestIp: string | null; } diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 80b5228bcd..ebc082dfbb 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -1,6 +1,6 @@ import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.js'; import { id } from '../id.js'; +import { User } from './user.js'; import { Clip } from './clip.js'; @Entity() @@ -188,6 +188,28 @@ export class Meta { }) public recaptchaSecretKey: string | null; + @Column('enum', { + enum: ['none', 'all', 'local', 'remote'], + default: 'none', + }) + public sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote'; + + @Column('enum', { + enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'], + default: 'medium', + }) + public sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh'; + + @Column('boolean', { + default: false, + }) + public setSensitiveFlagAutomatically: boolean; + + @Column('boolean', { + default: false, + }) + public enableSensitiveMediaDetectionForVideos: boolean; + @Column('integer', { default: 1024, comment: 'Drive capacity of a local user (MB)', @@ -427,4 +449,9 @@ export class Meta { default: true, }) public objectStorageS3ForcePathStyle: boolean; + + @Column('boolean', { + default: false, + }) + public enableIpLogging: boolean; } diff --git a/packages/backend/src/models/entities/user-ip.ts b/packages/backend/src/models/entities/user-ip.ts new file mode 100644 index 0000000000..543e9e7289 --- /dev/null +++ b/packages/backend/src/models/entities/user-ip.ts @@ -0,0 +1,24 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { id } from '../id.js'; +import { Note } from './note.js'; +import { User } from './user.js'; + +@Entity() +@Index(['userId', 'ip'], { unique: true }) +export class UserIp { + @PrimaryGeneratedColumn() + public id: string; + + @Column('timestamp with time zone', { + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @Column('varchar', { + length: 128, + }) + public ip: string; +} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index 1778742ea2..3654b0a994 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -1,8 +1,8 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { ffVisibility, notificationTypes } from '@/types.js'; import { id } from '../id.js'; import { User } from './user.js'; import { Page } from './page.js'; -import { ffVisibility, notificationTypes } from '@/types.js'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン @@ -117,6 +117,11 @@ export class UserProfile { }) public password: string | null; + @Column('varchar', { + length: 8192, default: '', + }) + public moderationNote: string | null; + // TODO: そのうち消す @Column('jsonb', { default: {}, @@ -150,6 +155,11 @@ export class UserProfile { @Column('boolean', { default: false, }) + public autoSensitive: boolean; + + @Column('boolean', { + default: false, + }) public carefulBot: boolean; @Column('boolean', { diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index df92fb8259..bc9446be41 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -218,6 +218,12 @@ export class User { }) public token: string | null; + @Column('integer', { + nullable: true, + comment: 'Overrides user drive capacity limit', + }) + public driveCapacityOverrideMb: number | null; + constructor(data: Partial<User>) { if (data == null) return; diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 814b37d448..3f73269318 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -65,6 +65,7 @@ import { PasswordResetRequest } from './entities/password-reset-request.js'; import { UserPending } from './entities/user-pending.js'; import { InstanceRepository } from './repositories/instance.js'; import { Webhook } from './entities/webhook.js'; +import { UserIp } from './entities/user-ip.js'; export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); @@ -90,6 +91,7 @@ export const UserGroups = (UserGroupRepository); export const UserGroupJoinings = db.getRepository(UserGroupJoining); export const UserGroupInvitations = (UserGroupInvitationRepository); export const UserNotePinings = db.getRepository(UserNotePining); +export const UserIps = db.getRepository(UserIp); export const UsedUsernames = db.getRepository(UsedUsername); export const Followings = (FollowingRepository); export const FollowRequests = (FollowRequestRepository); diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts index 4ddf717098..5f0fd8d582 100644 --- a/packages/backend/src/models/repositories/instance.ts +++ b/packages/backend/src/models/repositories/instance.ts @@ -1,11 +1,13 @@ import { db } from '@/db/postgre.js'; import { Instance } from '@/models/entities/instance.js'; import { Packed } from '@/misc/schema.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; export const InstanceRepository = db.getRepository(Instance).extend({ async pack( instance: Instance, ): Promise<Packed<'FederationInstance'>> { + const meta = await fetchMeta(); return { id: instance.id, caughtAt: instance.caughtAt.toISOString(), @@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({ lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), isNotResponding: instance.isNotResponding, isSuspended: instance.isSuspended, + isBlocked: meta.blockedHosts.includes(instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, @@ -26,6 +29,8 @@ export const InstanceRepository = db.getRepository(Instance).extend({ maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, }; }, diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 8a4e48efdd..5c46ae27a3 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({ } : undefined) : undefined, emojis: populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), + driveCapacityOverrideMb: user.driveCapacityOverrideMb, ...(opts.detail ? { url: profile!.url, @@ -359,6 +360,7 @@ export const UserRepository = db.getRepository(User).extend({ injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, alwaysMarkNsfw: profile!.alwaysMarkNsfw, + autoSensitive: profile!.autoSensitive, carefulBot: profile!.carefulBot, autoAcceptFollowed: profile!.autoAcceptFollowed, noCrawle: profile!.noCrawle, diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts index c4e7b3f18b..93327304f3 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/schema/federation-instance.ts @@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isBlocked: { + type: 'boolean', + optional: false, nullable: false, + }, softwareName: { type: 'string', optional: false, nullable: true, @@ -88,6 +92,15 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'url', }, + faviconUrl: { + type: 'string', + optional: false, nullable: true, + format: 'url', + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, infoUpdatedAt: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 253681695d..1c8fe97858 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -161,19 +161,19 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'array', nullable: false, optional: false, items: { - type: 'object', - nullable: false, optional: false, - properties: { - name: { - type: 'string', - nullable: false, optional: false, - }, - value: { - type: 'string', - nullable: false, optional: false, - }, + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, }, - maxLength: 4, + value: { + type: 'string', + nullable: false, optional: false, + }, + }, + maxLength: 4, }, }, followersCount: { @@ -292,6 +292,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: true, optional: false, }, + autoSensitive: { + type: 'boolean', + nullable: true, optional: false, + }, carefulBot: { type: 'boolean', nullable: true, optional: false, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index c5fd7de1cb..ebb3a77cab 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -2,6 +2,9 @@ import httpSignature from '@peertube/http-signature'; import { v4 as uuid } from 'uuid'; import config from '@/config/index.js'; +import { DriveFile } from '@/models/entities/drive-file.js'; +import { IActivity } from '@/remote/activitypub/type.js'; +import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; import { envOption } from '../env.js'; import processDeliver from './processors/deliver.js'; @@ -12,18 +15,15 @@ import processSystemQueue from './processors/system/index.js'; import processWebhookDeliver from './processors/webhook-deliver.js'; import { endedPollNotification } from './processors/ended-poll-notification.js'; import { queueLogger } from './logger.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; import { getJobInfo } from './get-job-info.js'; import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; import { ThinUser } from './types.js'; -import { IActivity } from '@/remote/activitypub/type.js'; -import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; function renderError(e: Error): any { return { - stack: e?.stack, - message: e?.message, - name: e?.name, + stack: e.stack, + message: e.message, + name: e.name, }; } @@ -314,6 +314,12 @@ export default function() { removeOnComplete: true, }); + systemQueue.add('clean', { + }, { + repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, + }); + systemQueue.add('checkExpiredMutings', { }, { repeat: { cron: '*/5 * * * *' }, diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts index 1db118ca9f..eef4080af3 100644 --- a/packages/backend/src/queue/initialize.ts +++ b/packages/backend/src/queue/initialize.ts @@ -6,6 +6,7 @@ export function initialize<T>(name: string, limitPerSec = -1) { redis: { port: config.redis.port, host: config.redis.host, + family: config.redis.family == null ? 0 : config.redis.family, password: config.redis.pass, db: config.redis.db || 0, }, diff --git a/packages/backend/src/queue/processors/system/clean.ts b/packages/backend/src/queue/processors/system/clean.ts new file mode 100644 index 0000000000..c4f978d7c9 --- /dev/null +++ b/packages/backend/src/queue/processors/system/clean.ts @@ -0,0 +1,18 @@ +import Bull from 'bull'; +import { LessThan } from 'typeorm'; +import { UserIps } from '@/models/index.js'; + +import { queueLogger } from '../../logger.js'; + +const logger = queueLogger.createSubLogger('clean'); + +export async function clean(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> { + logger.info('Cleaning...'); + + UserIps.delete({ + createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), + }); + + logger.succ('Cleaned.'); + done(); +} diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts index f90f6efafd..9527d40b0f 100644 --- a/packages/backend/src/queue/processors/system/index.ts +++ b/packages/backend/src/queue/processors/system/index.ts @@ -3,12 +3,14 @@ import { tickCharts } from './tick-charts.js'; import { resyncCharts } from './resync-charts.js'; import { cleanCharts } from './clean-charts.js'; import { checkExpiredMutings } from './check-expired-mutings.js'; +import { clean } from './clean.js'; const jobs = { tickCharts, resyncCharts, cleanCharts, checkExpiredMutings, + clean, } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>; export default function(dbQueue: Bull.Queue<Record<string, unknown>>) { diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 56c1a483ad..5d63f2605a 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -200,7 +200,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') { text = note.source.content; - } else if (typeof note._misskey_content === 'string') { + } else if (typeof note._misskey_content !== 'undefined') { text = note._misskey_content; } else if (typeof note.content === 'string') { text = htmlToMfm(note.content, note.tag); diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index df2ae65205..b3bafaa3ab 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -82,15 +82,14 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const files = await getPromisedFiles(note.fileIds); - // text should never be undefined - const text = note.text ?? null; + const text = note.text ?? ''; let poll: Poll | null = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); } - let apText = text ?? ''; + let apText = text; if (quote) { apText += `\n\nRE: ${quote}`; diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index 5d00481b75..de7eb0ed83 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -201,7 +201,7 @@ export interface IApMention extends IObject { href: string; } -export const isMention = (object: IObject): object is IApMention=> +export const isMention = (object: IObject): object is IApMention => getApType(object) === 'Mention' && typeof object.href === 'string'; diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index f97c3dd397..34ff970b4c 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -1,12 +1,25 @@ import Koa from 'koa'; +import { User } from '@/models/entities/user.js'; +import { UserIps } from '@/models/index.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; import { IEndpoint } from './endpoints.js'; import authenticate, { AuthenticationError } from './authenticate.js'; import call from './call.js'; import { ApiError } from './error.js'; +const userIpHistories = new Map<User['id'], Set<string>>(); + +setInterval(() => { + userIpHistories.clear(); +}, 1000 * 60 * 60); + export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => { - const body = ctx.request.body; + const body = ctx.is('multipart/form-data') + ? (ctx.request as any).body + : ctx.method === 'GET' + ? ctx.query + : ctx.request.body; const reply = (x?: any, y?: ApiError) => { if (x == null) { @@ -33,10 +46,38 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res authenticate(body['i']).then(([user, app]) => { // API invoking call(endpoint.name, user, app, body, ctx).then((res: any) => { + if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { + ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); + } reply(res); }).catch((e: ApiError) => { reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); }); + + // Log IP + if (user) { + fetchMeta().then(meta => { + if (!meta.enableIpLogging) return; + const ip = ctx.ip; + const ips = userIpHistories.get(user.id); + if (ips == null || !ips.has(ip)) { + if (ips == null) { + userIpHistories.set(user.id, new Set([ip])); + } else { + ips.add(ip); + } + + try { + UserIps.insert({ + createdAt: new Date(), + userId: user.id, + ip: ip, + }); + } catch { + } + } + }); + } }).catch(e => { if (e instanceof AuthenticationError) { reply(403, new ApiError({ diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index cd3e0abc06..aa130459a3 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,12 +1,12 @@ -import Koa from 'koa'; import { performance } from 'perf_hooks'; -import { limiter } from './limiter.js'; +import Koa from 'koa'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; +import { AccessToken } from '@/models/entities/access-token.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import { limiter } from './limiter.js'; import endpoints, { IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; const accessDenied = { message: 'Access denied.', @@ -33,7 +33,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied); } - if (ep.meta.limit && !isModerator) { + if (ep.meta.limit) { // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { @@ -94,7 +94,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi } // Cast non JSON input - if (ep.meta.requireFile && ep.params.properties) { + if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { const param = ep.params.properties![k]; if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { @@ -116,24 +116,24 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi // API invoking const before = performance.now(); - return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => { + return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => { if (e instanceof ApiError) { throw e; } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, { + apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { ep: ep.name, ps: data, e: { - message: e?.message, - code: e?.name, - stack: e?.stack, + message: e.message, + code: e.name, + stack: e.stack, }, }); throw new ApiError(null, { e: { - message: e?.message, - code: e?.name, - stack: e?.stack, + message: e.message, + code: e.name, + stack: e.stack, }, }); } diff --git a/packages/backend/src/server/api/common/generate-muted-instance-query.ts b/packages/backend/src/server/api/common/generate-muted-instance-query.ts deleted file mode 100644 index 72a6fec68f..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-instance-query.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { id } from '@/models/id.js'; -import { UserProfiles } from '@/models/index.js'; -import { SelectQueryBuilder, Brackets } from 'typeorm'; - -function createMutesQuery(id: string) { - return UserProfiles.createQueryBuilder('user_profile') - .select('user_profile.mutedInstances') - .where('user_profile.userId = :muterId', { muterId: id }); -} - -export function generateMutedInstanceQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const mutingQuery = createMutesQuery(me.id); - - q - .andWhere(new Brackets(qb => { qb - .andWhere('note.userHost IS NULL') - .orWhere(`NOT((${ mutingQuery.getQuery() })::jsonb ? note.userHost)`); - })) - .andWhere(new Brackets(qb => { qb - .where(`note.replyUserHost IS NULL`) - .orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.replyUserHost)`); - })) - .andWhere(new Brackets(qb => { qb - .where(`note.renoteUserHost IS NULL`) - .orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.renoteUserHost)`); - })); - q.setParameters(mutingQuery.getParameters()); -} - -export function generateMutedInstanceNotificationQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { - const mutingQuery = createMutesQuery(me.id); - - q.andWhere(new Brackets(qb => { qb - .andWhere('notifier.host IS NULL') - .orWhere(`NOT (( ${mutingQuery.getQuery()} )::jsonb ? notifier.host)`); - })); - - q.setParameters(mutingQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/generate-muted-user-query.ts b/packages/backend/src/server/api/common/generate-muted-user-query.ts index 79cb3ff894..470ece1a62 100644 --- a/packages/backend/src/server/api/common/generate-muted-user-query.ts +++ b/packages/backend/src/server/api/common/generate-muted-user-query.ts @@ -1,6 +1,6 @@ -import { User } from '@/models/entities/user.js'; -import { Mutings } from '@/models/index.js'; import { SelectQueryBuilder, Brackets } from 'typeorm'; +import { User } from '@/models/entities/user.js'; +import { Mutings, UserProfiles } from '@/models/index.js'; export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) { const mutingQuery = Mutings.createQueryBuilder('muting') @@ -11,21 +11,39 @@ export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: Use mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); } + const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: me.id }); + // 投稿の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ // 投稿の引用元の作者をミュートしていない q .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) .andWhere(new Brackets(qb => { qb - .where(`note.replyUserId IS NULL`) + .where('note.replyUserId IS NULL') .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); })) .andWhere(new Brackets(qb => { qb - .where(`note.renoteUserId IS NULL`) + .where('note.renoteUserId IS NULL') .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + // mute instances + .andWhere(new Brackets(qb => { qb + .andWhere('note.userHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.replyUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where('note.renoteUserHost IS NULL') + .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); })); q.setParameters(mutingQuery.getParameters()); + q.setParameters(mutingInstanceQuery.getParameters()); } export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { @@ -33,8 +51,7 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - q - .andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); + q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); q.setParameters(mutingQuery.getParameters()); } diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts index 1529894341..c1b56b8a83 100644 --- a/packages/backend/src/server/api/define.ts +++ b/packages/backend/src/server/api/define.ts @@ -1,16 +1,16 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; -import { IEndpointMeta } from './endpoints.js'; -import { ApiError } from './error.js'; import { Schema, SchemaType } from '@/misc/schema.js'; import { AccessToken } from '@/models/entities/access-token.js'; +import { IEndpointMeta } from './endpoints.js'; +import { ApiError } from './error.js'; export type Response = Record<string, any> | void; // TODO: paramsの型をT['params']のスキーマ定義から推論する type executor<T extends IEndpointMeta, Ps extends Schema> = - (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => + (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) => Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; const ajv = new Ajv({ @@ -20,24 +20,27 @@ const ajv = new Ajv({ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>) - : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> { - + : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> { const validate = ajv.compile(paramDef); - return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { - function cleanup() { - fs.unlink(file.path, () => {}); - } + return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => { + let cleanup: undefined | (() => void) = undefined; - if (meta.requireFile && file == null) return Promise.reject(new ApiError({ - message: 'File required.', - code: 'FILE_REQUIRED', - id: '4267801e-70d1-416a-b011-4ee502885d8b', - })); + if (meta.requireFile) { + cleanup = () => { + fs.unlink(file.path, () => {}); + }; + + if (file == null) return Promise.reject(new ApiError({ + message: 'File required.', + code: 'FILE_REQUIRED', + id: '4267801e-70d1-416a-b011-4ee502885d8b', + })); + } const valid = validate(params); if (!valid) { - if (file) cleanup(); + if (file) cleanup!(); const errors = validate.errors!; const err = new ApiError({ @@ -51,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa return Promise.reject(err); } - return cb(params as SchemaType<Ps>, user, token, file, cleanup); + return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers); }; } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 1e7afd8cdd..4644f34d94 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; +import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; import * as ep___admin_invite from './endpoints/admin/invite.js'; import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; @@ -59,6 +60,8 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; +import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; +import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -99,6 +102,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; import * as ep___charts_users from './endpoints/charts/users.js'; import * as ep___clips_addNote from './endpoints/clips/add-note.js'; +import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; import * as ep___clips_create from './endpoints/clips/create.js'; import * as ep___clips_delete from './endpoints/clips/delete.js'; import * as ep___clips_list from './endpoints/clips/list.js'; @@ -133,6 +137,7 @@ import * as ep___federation_instances from './endpoints/federation/instances.js' import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; import * as ep___federation_users from './endpoints/federation/users.js'; +import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; @@ -308,6 +313,8 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___fetchRss from './endpoints/fetch-rss.js'; +import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -345,6 +352,7 @@ const eps = [ ['admin/federation/update-instance', ep___admin_federation_updateInstance], ['admin/get-index-stats', ep___admin_getIndexStats], ['admin/get-table-stats', ep___admin_getTableStats], + ['admin/get-user-ips', ep___admin_getUserIps], ['admin/invite', ep___admin_invite], ['admin/moderators/add', ep___admin_moderators_add], ['admin/moderators/remove', ep___admin_moderators_remove], @@ -369,6 +377,8 @@ const eps = [ ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/vacuum', ep___admin_vacuum], + ['admin/delete-account', ep___admin_deleteAccount], + ['admin/update-user-note', ep___admin_updateUserNote], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], @@ -409,6 +419,7 @@ const eps = [ ['charts/user/reactions', ep___charts_user_reactions], ['charts/users', ep___charts_users], ['clips/add-note', ep___clips_addNote], + ['clips/remove-note', ep___clips_removeNote], ['clips/create', ep___clips_create], ['clips/delete', ep___clips_delete], ['clips/list', ep___clips_list], @@ -443,6 +454,7 @@ const eps = [ ['federation/show-instance', ep___federation_showInstance], ['federation/update-remote-user', ep___federation_updateRemoteUser], ['federation/users', ep___federation_users], + ['federation/stats', ep___federation_stats], ['following/create', ep___following_create], ['following/delete', ep___following_delete], ['following/invalidate', ep___following_invalidate], @@ -618,6 +630,8 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], + ['admin/drive-capacity-override', ep___admin_driveCapOverride], + ['fetch-rss', ep___fetchRss], ]; export interface IEndpointMeta { @@ -699,6 +713,16 @@ export interface IEndpointMeta { readonly kind?: string; readonly description?: string; + + /** + * GETでのリクエストを許容するか否か + */ + readonly allowGet?: boolean; + + /** + * 正常応答をキャッシュ (Cache-Control: public) する秒数 + */ + readonly cacheSec?: number; } export interface IEndpoint { diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts new file mode 100644 index 0000000000..2d7ef2f236 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -0,0 +1,31 @@ +import { Users } from '@/models/index.js'; +import { deleteAccount } from '@/services/delete-account.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireAdmin: true, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps) => { + const user = await Users.findOneByOrFail({ id: ps.userId }); + if (user.isDeleted) { + return; + } + + await deleteAccount(user); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts new file mode 100644 index 0000000000..a4b29770e1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts @@ -0,0 +1,47 @@ +import define from '../../define.js'; +import { Users } from '@/models/index.js'; +import { User } from '@/models/entities/user.js'; +import { insertModerationLog } from '@/services/insert-moderation-log.js'; +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + overrideMb: { type: 'number', nullable: true }, + }, + required: ['userId', 'overrideMb'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const user = await Users.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (!Users.isLocalUser(user)) { + throw new Error('user is not local user'); + } + + /*if (user.isAdmin) { + throw new Error('cannot suspend admin'); + } + if (user.isModerator) { + throw new Error('cannot suspend moderator'); + }*/ + + await Users.update(user.id, { + driveCapacityOverrideMb: ps.overrideMb, + }); + + insertModerationLog(me, 'change-drive-capacity-override', { + targetId: user.id, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 119c4db19b..ba32aac431 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { DriveFiles } from '@/models/index.js'; +import define from '../../../define.js'; import { makePaginationQuery } from '../../../common/make-pagination-query.js'; export const meta = { @@ -25,8 +25,9 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, hostname: { type: 'string', nullable: true, @@ -41,14 +42,18 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); - if (ps.origin === 'local') { - query.andWhere('file.userHost IS NULL'); - } else if (ps.origin === 'remote') { - query.andWhere('file.userHost IS NOT NULL'); - } + if (ps.userId) { + query.andWhere('file.userId = :userId', { userId: ps.userId }); + } else { + if (ps.origin === 'local') { + query.andWhere('file.userHost IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('file.userHost IS NOT NULL'); + } - if (ps.hostname) { - query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); + if (ps.hostname) { + query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); + } } if (ps.type) { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 039df74f1b..e9117a23c8 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -1,6 +1,6 @@ +import { DriveFiles } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles } from '@/models/index.js'; export const meta = { tags: ['admin'], @@ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(meta.errors.noSuchFile); } + if (!me.isAdmin) { + delete file.requestIp; + delete file.requestHeaders; + } + return file; }); diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts new file mode 100644 index 0000000000..e8b9cb3b09 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -0,0 +1,31 @@ +import { UserIps } from '@/models/index.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireAdmin: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const ips = await UserIps.find({ + where: { userId: ps.userId }, + order: { createdAt: 'DESC' }, + take: 30, + }); + + return ips.map(x => ({ + ip: x.ip, + createdAt: x.createdAt.toISOString(), + })); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 8d50486ef6..cb50e128af 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,7 +1,7 @@ import config from '@/config/index.js'; -import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import define from '../../define.js'; export const meta = { tags: ['meta'], @@ -195,6 +195,22 @@ export const meta = { type: 'string', optional: true, nullable: true, }, + sensitiveMediaDetection: { + type: 'string', + optional: true, nullable: false, + }, + sensitiveMediaDetectionSensitivity: { + type: 'string', + optional: true, nullable: false, + }, + setSensitiveFlagAutomatically: { + type: 'boolean', + optional: true, nullable: false, + }, + enableSensitiveMediaDetectionForVideos: { + type: 'boolean', + optional: true, nullable: false, + }, proxyAccountId: { type: 'string', optional: true, nullable: true, @@ -304,6 +320,10 @@ export const meta = { type: 'boolean', optional: true, nullable: false, }, + enableIpLogging: { + type: 'boolean', + optional: true, nullable: false, + }, }, }, } as const; @@ -360,13 +380,16 @@ export default define(meta, paramDef, async (ps, me) => { pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, - useStarForReactionFallback: instance.useStarForReactionFallback, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, hcaptchaSecretKey: instance.hcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, + sensitiveMediaDetection: instance.sensitiveMediaDetection, + sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, + setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, proxyAccountId: instance.proxyAccountId, twitterConsumerKey: instance.twitterConsumerKey, twitterConsumerSecret: instance.twitterConsumerSecret, @@ -397,5 +420,6 @@ export default define(meta, paramDef, async (ps, me) => { objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, deeplAuthKey: instance.deeplAuthKey, deeplIsPro: instance.deeplIsPro, + enableIpLogging: instance.enableIpLogging, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 9c150420b1..85c6fb82e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -99,12 +99,16 @@ export default define(meta, paramDef, async () => { const fsStats = await si.fsSize(); const netInterface = await si.networkInterfaceDefault(); + const redisServerInfo = await redisClient.info('Server'); + const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm')); + const redis_version = m?.[1]; + return { machine: os.hostname(), os: os.platform(), node: process.version, psql: await db.query('SHOW server_version').then(x => x[0].server_version), - redis: redisClient.server_info.redis_version, + redis: redis_version, cpu: { model: os.cpus()[0].model, cores: os.cpus().length, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 78033aed58..0d866b3113 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -25,7 +25,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const [user, profile] = await Promise.all([ Users.findOneBy({ id: ps.userId }), - UserProfiles.findOneBy({ userId: ps.userId }) + UserProfiles.findOneBy({ userId: ps.userId }), ]); if (user == null || profile == null) { @@ -58,6 +58,7 @@ export default define(meta, paramDef, async (ps, me) => { autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, alwaysMarkNsfw: profile.alwaysMarkNsfw, + autoSensitive: profile.autoSensitive, carefulBot: profile.carefulBot, injectFeaturedNote: profile.injectFeaturedNote, receiveAnnouncementEmail: profile.receiveAnnouncementEmail, @@ -68,6 +69,8 @@ export default define(meta, paramDef, async (ps, me) => { isModerator: user.isModerator, isSilenced: user.isSilenced, isSuspended: user.isSuspended, + lastActiveDate: user.lastActiveDate, + moderationNote: profile.moderationNote, signins, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 1575d81d5d..8e09e72d5b 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -25,7 +25,7 @@ export const paramDef = { offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { type: 'string', @@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (ps.hostname) { - query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' }); + query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); } switch (ps.sort) { diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 09e43301b7..cc32e73c53 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; import { Meta } from '@/models/entities/meta.js'; import { insertModerationLog } from '@/services/insert-moderation-log.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; import { db } from '@/db/postgre.js'; +import define from '../../define.js'; export const meta = { tags: ['admin'], @@ -48,6 +48,10 @@ export const paramDef = { enableRecaptcha: { type: 'boolean' }, recaptchaSiteKey: { type: 'string', nullable: true }, recaptchaSecretKey: { type: 'string', nullable: true }, + sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, + sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, + setSensitiveFlagAutomatically: { type: 'boolean' }, + enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, @@ -96,6 +100,7 @@ export const paramDef = { objectStorageUseProxy: { type: 'boolean' }, objectStorageSetPublicRead: { type: 'boolean' }, objectStorageS3ForcePathStyle: { type: 'boolean' }, + enableIpLogging: { type: 'boolean' }, }, required: [], } as const; @@ -212,6 +217,22 @@ export default define(meta, paramDef, async (ps, me) => { set.recaptchaSecretKey = ps.recaptchaSecretKey; } + if (ps.sensitiveMediaDetection !== undefined) { + set.sensitiveMediaDetection = ps.sensitiveMediaDetection; + } + + if (ps.sensitiveMediaDetectionSensitivity !== undefined) { + set.sensitiveMediaDetectionSensitivity = ps.sensitiveMediaDetectionSensitivity; + } + + if (ps.setSensitiveFlagAutomatically !== undefined) { + set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically; + } + + if (ps.enableSensitiveMediaDetectionForVideos !== undefined) { + set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; + } + if (ps.proxyAccountId !== undefined) { set.proxyAccountId = ps.proxyAccountId; } @@ -396,6 +417,10 @@ export default define(meta, paramDef, async (ps, me) => { set.deeplIsPro = ps.deeplIsPro; } + if (ps.enableIpLogging !== undefined) { + set.enableIpLogging = ps.enableIpLogging; + } + await db.transaction(async transactionalEntityManager => { const metas = await transactionalEntityManager.find(Meta, { order: { diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts new file mode 100644 index 0000000000..fa21ab7833 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -0,0 +1,31 @@ +import { UserProfiles, Users } from '@/models/index.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + text: { type: 'string' }, + }, + required: ['userId', 'text'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const user = await Users.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await UserProfiles.update({ userId: user.id }, { + moderationNote: ps.text, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 222efdcef0..23cb93c9a5 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Announcements, AnnouncementReads } from '@/models/index.js'; +import define from '../define.js'; import { makePaginationQuery } from '../common/make-pagination-query.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 3c0c0642e3..6442a1412c 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -2,12 +2,13 @@ import define from '../../define.js'; import config from '@/config/index.js'; import { createPerson } from '@/remote/activitypub/models/person.js'; import { createNote } from '@/remote/activitypub/models/note.js'; +import DbResolver from '@/remote/activitypub/db-resolver.js'; import Resolver from '@/remote/activitypub/resolver.js'; import { ApiError } from '../../error.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; +import { CacheableLocalUser, User } from '@/models/entities/user.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { isActor, isPost, getApId } from '@/remote/activitypub/type.js'; import ms from 'ms'; @@ -77,8 +78,8 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const object = await fetchAny(ps.uri); +export default define(meta, paramDef, async (ps, me) => { + const object = await fetchAny(ps.uri, me); if (object) { return object; } else { @@ -89,48 +90,18 @@ export default define(meta, paramDef, async (ps) => { /*** * URIからUserかNoteを解決する */ -async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | null> { - // URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ - if (uri.startsWith(config.url + '/')) { - const parts = uri.split('/'); - const id = parts.pop(); - const type = parts.pop(); - - if (type === 'notes') { - const note = await Notes.findOneBy({ id }); - - if (note) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }), - }; - } - } else if (type === 'users') { - const user = await Users.findOneBy({ id }); - - if (user) { - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }), - }; - } - } - } - +async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { // ブロックしてたら中断 const fetchedMeta = await fetchMeta(); if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null; - // URI(AP Object id)としてDB検索 - { - const [user, note] = await Promise.all([ - Users.findOneBy({ uri: uri }), - Notes.findOneBy({ uri: uri }), - ]); + const dbResolver = new DbResolver(); - const packed = await mergePack(user, note); - if (packed !== null) return packed; - } + let local = await mergePack(me, ...await Promise.all([ + dbResolver.getUserFromApId(uri), + dbResolver.getNoteFromApId(uri), + ])); + if (local != null) return local; // リモートから一旦オブジェクトフェッチ const resolver = new Resolver(); @@ -139,74 +110,37 @@ async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | n // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する // これはDBに存在する可能性があるため再度DB検索 if (uri !== object.id) { - if (object.id.startsWith(config.url + '/')) { - const parts = object.id.split('/'); - const id = parts.pop(); - const type = parts.pop(); - - if (type === 'notes') { - const note = await Notes.findOneBy({ id }); - - if (note) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }), - }; - } - } else if (type === 'users') { - const user = await Users.findOneBy({ id }); - - if (user) { - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }), - }; - } - } - } - - const [user, note] = await Promise.all([ - Users.findOneBy({ uri: object.id }), - Notes.findOneBy({ uri: object.id }), - ]); - - const packed = await mergePack(user, note); - if (packed !== null) return packed; - } - - // それでもみつからなければ新規であるため登録 - if (isActor(object)) { - const user = await createPerson(getApId(object)); - return { - type: 'User', - object: await Users.pack(user, null, { detail: true }), - }; - } - - if (isPost(object)) { - const note = await createNote(getApId(object), undefined, true); - return { - type: 'Note', - object: await Notes.pack(note!, null, { detail: true }), - }; + local = await mergePack(me, ...await Promise.all([ + dbResolver.getUserFromApId(object.id), + dbResolver.getNoteFromApId(object.id), + ])); + if (local != null) return local; } - return null; + return await mergePack( + me, + isActor(object) ? await createPerson(getApId(object)) : null, + isPost(object) ? await createNote(getApId(object), undefined, true) : null, + ); } -async function mergePack(user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> { +async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> { if (user != null) { return { type: 'User', - object: await Users.pack(user, null, { detail: true }), + object: await Users.pack(user, me, { detail: true }), }; - } + } else if (note != null) { + try { + const object = await Notes.pack(note, me, { detail: true }); - if (note != null) { - return { - type: 'Note', - object: await Notes.pack(note, null, { detail: true }), - }; + return { + type: 'Note', + object, + }; + } catch (e) { + return null; + } } return null; diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index 97f7885dbe..ea23794296 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'users'], res: getJsonSchema(activeUsersChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index 4477bfc987..06dee250ee 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { apRequestChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(apRequestChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index fd6033392f..dd2c2d6838 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { driveChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'drive'], res: getJsonSchema(driveChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index f842f574ec..8c35b3c46d 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { federationChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(federationChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts index 01407defdd..77e24a62c3 100644 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { hashtagChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'hashtags'], res: getJsonSchema(hashtagChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 2d12951c6c..817d51ad01 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { instanceChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(instanceChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index b6089f67ef..951adf5408 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { notesChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'notes'], res: getJsonSchema(notesChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index e5db7131a8..f165b40224 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserDriveChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'drive', 'users'], res: getJsonSchema(perUserDriveChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 9b72de745d..f5d42e21c2 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -6,6 +6,9 @@ export const meta = { tags: ['charts', 'users', 'following'], res: getJsonSchema(perUserFollowingChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 7cc6cbf316..aefe550d43 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserNotesChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'users', 'notes'], res: getJsonSchema(perUserNotesChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 5c58a7f152..6bc6b56bf0 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserReactionsChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'users', 'reactions'], res: getJsonSchema(perUserReactionsChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 49c762b2e4..338e8fd338 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { usersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'users'], res: getJsonSchema(usersChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts new file mode 100644 index 0000000000..8b90e31f65 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -0,0 +1,57 @@ +import define from '../../define.js';
+import { ClipNotes, Clips } from '@/models/index.js';
+import { ApiError } from '../../error.js';
+import { getNote } from '../../common/getters.js';
+
+export const meta = {
+ tags: ['account', 'notes', 'clips'],
+
+ requireCredential: true,
+
+ kind: 'write:account',
+
+ errors: {
+ noSuchClip: {
+ message: 'No such clip.',
+ code: 'NO_SUCH_CLIP',
+ id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
+ },
+
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: 'aff017de-190e-434b-893e-33a9ff5049d8',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ clipId: { type: 'string', format: 'misskey:id' },
+ noteId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['clipId', 'noteId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps, user) => {
+ const clip = await Clips.findOneBy({
+ id: ps.clipId,
+ userId: user.id,
+ });
+
+ if (clip == null) {
+ throw new ApiError(meta.errors.noSuchClip);
+ }
+
+ const note = await getNote(ps.noteId).catch(e => {
+ if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw e;
+ });
+
+ await ClipNotes.delete({
+ noteId: note.id,
+ clipId: clip.id,
+ });
+});
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index c599d96ca4..82497adefa 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { DriveFiles } from '@/models/index.js'; +import define from '../define.js'; export const meta = { tags: ['drive', 'account'], @@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => { const usage = await DriveFiles.calcDriveUsageOf(user.id); return { - capacity: 1024 * 1024 * instance.localDriveCapacityMb, + capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb), usage: usage, }; }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 7397fd9ce9..ddcbd62889 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,10 +1,12 @@ import ms from 'ms'; import { addFile } from '@/services/drive/add-file.js'; +import { DriveFiles } from '@/models/index.js'; +import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; import define from '../../../define.js'; import { apiLogger } from '../../../logger.js'; import { ApiError } from '../../../error.js'; -import { DriveFiles } from '@/models/index.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; export const meta = { tags: ['drive'], @@ -34,6 +36,18 @@ export const meta = { code: 'INVALID_FILE_NAME', id: 'f449b209-0c60-4e51-84d5-29486263bfd4', }, + + inappropriate: { + message: 'Cannot upload the file because it has been determined that it possibly contains inappropriate content.', + code: 'INAPPROPRIATE', + id: 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2', + }, + + noFreeSpace: { + message: 'Cannot upload the file because you have no free space of drive.', + code: 'NO_FREE_SPACE', + id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', + }, }, } as const; @@ -50,7 +64,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { +export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => { // Get 'name' parameter let name = ps.name || file.originalname; if (name !== undefined && name !== null) { @@ -66,14 +80,30 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { name = null; } + const meta = await fetchMeta(); + try { // Create file - const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive }); + const driveFile = await addFile({ + user, + path: file.path, + name, + comment: ps.comment, + folderId: ps.folderId, + force: ps.force, + sensitive: ps.isSensitive, + requestIp: meta.enableIpLogging ? ip : null, + requestHeaders: meta.enableIpLogging ? headers : null, + }); return await DriveFiles.pack(driveFile, { self: true }); } catch (e) { if (e instanceof Error || typeof e === 'string') { apiLogger.error(e); } + if (e instanceof IdentifiableError) { + if (e.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); + if (e.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); + } throw new ApiError(); } finally { cleanup!(); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index e3debe0b4f..fa2ec8519c 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,8 +1,8 @@ import { publishDriveStream } from '@/services/stream.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; import { DriveFiles, DriveFolders, Users } from '@/models/index.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['drive'], diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 53f2298f21..eb8071c3c9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,9 +1,9 @@ import ms from 'ms'; import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import define from '../../../define.js'; import { DriveFiles } from '@/models/index.js'; import { publishMainStream } from '@/services/stream.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; +import define from '../../../define.js'; export const meta = { tags: ['drive'], @@ -34,8 +34,8 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => { +export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { + uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { DriveFiles.pack(file, { self: true }).then(packedFile => { publishMainStream(user.id, 'urlUploadFinished', { marker: ps.marker, diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index bc8d2e2ac0..5fe622932d 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; -import { createExportCustomEmojisJob } from '@/queue/index.js'; import ms from 'ms'; +import { createExportCustomEmojisJob } from '@/queue/index.js'; +import define from '../define.js'; export const meta = { secure: true, diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts new file mode 100644 index 0000000000..e02c7b97e0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -0,0 +1,65 @@ +import { IsNull, MoreThan, Not } from 'typeorm'; +import { Followings, Instances } from '@/models/index.js'; +import { awaitAll } from '@/prelude/await-all.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['federation'], + + requireCredential: false, + + allowGet: true, + cacheSec: 60 * 60, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps) => { + const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([ + Instances.find({ + where: { + followersCount: MoreThan(0), + }, + order: { + followersCount: 'DESC', + }, + take: ps.limit, + }), + Instances.find({ + where: { + followingCount: MoreThan(0), + }, + order: { + followingCount: 'DESC', + }, + take: ps.limit, + }), + Followings.count({ + where: { + followeeHost: Not(IsNull()), + }, + }), + Followings.count({ + where: { + followerHost: Not(IsNull()), + }, + }), + ]); + + const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0); + const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); + + return await awaitAll({ + topSubInstances: Instances.packMany(topSubInstances), + otherFollowersCount: Math.max(0, allSubCount - gotSubCount), + topPubInstances: Instances.packMany(topPubInstances), + otherFollowingCount: Math.max(0, allPubCount - gotPubCount), + }); +}); diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts new file mode 100644 index 0000000000..05fa22a9e4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -0,0 +1,39 @@ +import Parser from 'rss-parser'; +import { getResponse } from '@/misc/fetch.js'; +import config from '@/config/index.js'; +import define from '../define.js'; + +const rssParser = new Parser(); + +export const meta = { + tags: ['meta'], + + requireCredential: false, + allowGet: true, + cacheSec: 60 * 3, +} as const; + +export const paramDef = { + type: 'object', + properties: { + url: { type: 'string' }, + }, + required: ['url'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps) => { + const res = await getResponse({ + url: ps.url, + method: 'GET', + headers: Object.assign({ + 'User-Agent': config.userAgent, + Accept: 'application/rss+xml, */*', + }), + timeout: 5000, + }); + + const text = await res.text(); + + return rssParser.parseString(text); +}); diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index b0c1225bee..56c5502978 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,6 +1,6 @@ +import { MoreThan } from 'typeorm'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; import { Users } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; import define from '../define.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 5b1ad2b098..22aedfeee8 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Users } from '@/models/index.js'; +import define from '../define.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 184005eb53..ede4a9d03b 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,9 +1,7 @@ import bcrypt from 'bcryptjs'; -import define from '../../define.js'; import { UserProfiles, Users } from '@/models/index.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; +import { deleteAccount } from '@/services/delete-account.js'; +import define from '../../define.js'; export const meta = { requireCredential: true, @@ -34,17 +32,5 @@ export default define(meta, paramDef, async (ps, user) => { throw new Error('incorrect password'); } - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - - await Users.update(user.id, { - isDeleted: true, - }); - - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); + await deleteAccount(user); }); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 6ea8cb3574..a2249803ee 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,17 +1,21 @@ +import { Brackets } from 'typeorm'; +import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js'; +import { notificationTypes } from '@/types.js'; +import read from '@/services/note/read.js'; import { readNotification } from '../../common/read-notification.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query.js'; -import { Notifications, Followings, Mutings, Users } from '@/models/index.js'; -import { notificationTypes } from '@/types.js'; -import read from '@/services/note/read.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['account', 'notifications'], requireCredential: true, + limit: { + duration: 60000, + max: 10, + }, + kind: 'read:notifications', res: { @@ -62,12 +66,16 @@ export default define(meta, paramDef, async (ps, user) => { .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: user.id }); + const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: user.id }); + const suspendedQuery = Users.createQueryBuilder('users') .select('users.id') .where('users.isSuspended = TRUE'); const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) - .andWhere(`notification.notifieeId = :meId`, { meId: user.id }) + .andWhere('notification.notifieeId = :meId', { meId: user.id }) .leftJoinAndSelect('notification.notifier', 'notifier') .leftJoinAndSelect('notification.note', 'note') .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') @@ -84,14 +92,21 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + // muted users query.andWhere(new Brackets(qb => { qb .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) .orWhere('notification.notifierId IS NULL'); })); query.setParameters(mutingQuery.getParameters()); - generateMutedInstanceNotificationQuery(query, user); + // muted instances + query.andWhere(new Brackets(qb => { qb + .andWhere('notifier.host IS NULL') + .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); + })); + query.setParameters(mutingInstanceQuery.getParameters()); + // suspended users query.andWhere(new Brackets(qb => { qb .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) .orWhere('notification.notifierId IS NULL'); @@ -103,13 +118,13 @@ export default define(meta, paramDef, async (ps, user) => { } if (ps.includeTypes && ps.includeTypes.length > 0) { - query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes }); + query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes }); } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { - query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes }); + query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes }); } if (ps.unreadOnly) { - query.andWhere(`notification.isRead = false`); + query.andWhere('notification.isRead = false'); } const notifications = await query.take(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b2964e68c7..122120f275 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -3,17 +3,17 @@ import * as mfm from 'mfm-js'; import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import acceptAllFollowRequests from '@/services/following/requests/accept-all.js'; import { publishToFollowers } from '@/services/i/update.js'; -import define from '../../define.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import { updateUsertags } from '@/services/update-hashtag.js'; -import { ApiError } from '../../error.js'; import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index.js'; import { User } from '@/models/entities/user.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; +import { ApiError } from '../../error.js'; +import define from '../../define.js'; export const meta = { tags: ['account'], @@ -57,7 +57,7 @@ export const meta = { message: 'Invalid Regular Expression.', code: 'INVALID_REGEXP', id: '0d786918-10df-41cd-8f33-8dec7d9a89a5', - } + }, }, res: { @@ -77,7 +77,8 @@ export const paramDef = { lang: { type: 'string', enum: [null, ...Object.keys(langmap)], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, - fields: { type: 'array', + fields: { + type: 'array', minItems: 0, maxItems: 16, items: { @@ -102,6 +103,7 @@ export const paramDef = { injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, + autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'array', items: { type: 'string', format: 'misskey:id', @@ -168,6 +170,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; + if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index e1ae282a97..5b624842c3 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,10 +1,10 @@ +import { IsNull, MoreThan } from 'typeorm'; import config from '@/config/index.js'; -import define from '../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Ads, Emojis, Users } from '@/models/index.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; -import { IsNull, MoreThan } from 'typeorm'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import define from '../define.js'; export const meta = { tags: ['meta'], diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 2733c826e9..015b0338e3 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,6 +1,6 @@ +import { Notes } from '@/models/index.js'; import define from '../define.js'; import { makePaginationQuery } from '../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], @@ -34,8 +34,8 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.visibility = 'public'`) - .andWhere(`note.localOnly = FALSE`) + .andWhere('note.visibility = \'public\'') + .andWhere('note.localOnly = FALSE') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps) => { } if (ps.withFiles !== undefined) { - query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); + query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); } if (ps.poll !== undefined) { diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 86dde30d64..efc109105c 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,11 +1,10 @@ +import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Brackets } from 'typeorm'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; export const meta = { tags: ['notes'], @@ -38,13 +37,13 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { qb - .where(`note.replyId = :noteId`, { noteId: ps.noteId }) + .where('note.replyId = :noteId', { noteId: ps.noteId }) .orWhere(new Brackets(qb => { qb - .where(`note.renoteId = :noteId`, { noteId: ps.noteId }) + .where('note.renoteId = :noteId', { noteId: ps.noteId }) .andWhere(new Brackets(qb => { qb - .where(`note.text IS NOT NULL`) - .orWhere(`note.fileIds != '{}'`) - .orWhere(`note.hasPoll = TRUE`); + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); })); })); })) @@ -61,9 +60,10 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - if (user) generateMutedInstanceQuery(query, user); + if (user) { + generateMutedUserQuery(query, user); + generateBlockedUserQuery(query, user); + } const notes = await query.take(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 8683a7f75a..e79f8563e8 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; +import { In } from 'typeorm'; import { ClipNotes, Clips } from '@/models/index.js'; +import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { In } from 'typeorm'; export const meta = { tags: ['clips', 'notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index b991a495f2..b731d18248 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,8 +1,8 @@ +import { Note } from '@/models/entities/note.js'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 804e146fa4..c23ceeb5bf 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,9 +1,9 @@ +import ms from 'ms'; import deleteNote from '@/services/note/delete.js'; +import { Users } from '@/models/index.js'; import define from '../../define.js'; -import ms from 'ms'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 41dc5ac8e1..097371a425 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,8 +1,8 @@ +import { NoteFavorites } from '@/models/index.js'; +import { genId } from '@/misc/gen-id.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getNote } from '../../../common/getters.js'; -import { NoteFavorites } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['notes', 'favorites'], diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index a48f7a0aa8..82ef4fa197 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,7 +1,7 @@ +import { NoteFavorites } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getNote } from '../../../common/getters.js'; -import { NoteFavorites } from '@/models/index.js'; export const meta = { tags: ['notes', 'favorites'], diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 6308d23696..dd9cc581aa 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,6 +1,6 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { @@ -36,9 +36,9 @@ export default define(meta, paramDef, async (ps, user) => { const query = Notes.createQueryBuilder('note') .addSelect('note.score') .where('note.userHost IS NULL') - .andWhere(`note.score > 0`) - .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) - .andWhere(`note.visibility = 'public'`) + .andWhere('note.score > 0') + .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) + .andWhere('note.visibility = \'public\'') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index cb402ecaa1..925318f544 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,11 +1,10 @@ -import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Notes } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes, Users } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; @@ -60,7 +59,7 @@ export default define(meta, paramDef, async (ps, user) => { //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.visibility = \'public\'') .andWhere('note.channelId IS NULL') .innerJoinAndSelect('note.user', 'user') @@ -76,10 +75,11 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); generateRepliesQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateMutedNoteQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - if (user) generateMutedInstanceQuery(query, user); + if (user) { + generateMutedUserQuery(query, user); + generateMutedNoteQuery(query, user); + generateBlockedUserQuery(query, user); + } if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index f9893527e0..2dc98c4c9f 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,13 +1,12 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Followings, Notes } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Followings, Notes, Users } from '@/models/index.js'; -import { Brackets } from 'typeorm'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -70,7 +69,7 @@ export default define(meta, paramDef, async (ps, user) => { .where('following.followerId = :followerId', { followerId: user.id }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); @@ -92,7 +91,6 @@ export default define(meta, paramDef, async (ps, user) => { generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); - generateMutedInstanceQuery(query, user); generateMutedNoteQuery(query, user); generateBlockedUserQuery(query, user); @@ -134,9 +132,7 @@ export default define(meta, paramDef, async (ps, user) => { const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } + activeUsersChart.read(user); }); return await Notes.packMany(timeline, user); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 03edf30b31..aac2a3749c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,12 +1,12 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { ApiError } from '../../error.js'; import { Notes, Users } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -66,7 +66,7 @@ export default define(meta, paramDef, async (ps, user) => { //#region Construct query const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index eafbba322d..9b41544523 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,10 +1,10 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import read from '@/services/note/read.js'; import { Notes, Followings } from '@/models/index.js'; +import define from '../../define.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 28bfade2f0..5a04d68f3e 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; -import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js'; import { Brackets, In } from 'typeorm'; +import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['notes'], @@ -31,8 +31,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = Polls.createQueryBuilder('poll') .where('poll.userHost IS NULL') - .andWhere(`poll.userId != :meId`, { meId: user.id }) - .andWhere(`poll.noteVisibility = 'public'`) + .andWhere('poll.userId != :meId', { meId: user.id }) + .andWhere('poll.noteVisibility = \'public\'') .andWhere(new Brackets(qb => { qb .where('poll.expiresAt IS NULL') .orWhere('poll.expiresAt > :now', { now: new Date() }); @@ -60,12 +60,21 @@ export default define(meta, paramDef, async (ps, user) => { query.setParameters(mutingQuery.getParameters()); //#endregion - const polls = await query.take(ps.limit).skip(ps.offset).getMany(); + const polls = await query + .orderBy('poll.noteId', 'DESC') + .take(ps.limit) + .skip(ps.offset) + .getMany(); if (polls.length === 0) return []; - const notes = await Notes.findBy({ - id: In(polls.map(poll => poll.noteId)), + const notes = await Notes.find({ + where: { + id: In(polls.map(poll => poll.noteId)), + }, + order: { + createdAt: 'DESC', + }, }); return await Notes.packMany(notes, user, { diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 6244b55cf2..45a832cbd2 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,16 +1,16 @@ +import { Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import { createNotification } from '@/services/create-notification.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getNote } from '../../../common/getters.js'; import { deliver } from '@/queue/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderVote from '@/remote/activitypub/renderer/vote.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js'; -import { Not } from 'typeorm'; import { IRemoteUser } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; +import { getNote } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; +import define from '../../../define.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index fbb065329c..15a62d394d 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -9,6 +9,9 @@ export const meta = { requireCredential: false, + allowGet: true, + cacheSec: 60, + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index 639ecae264..c13cafa21d 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; import ms from 'ms'; import deleteReaction from '@/services/note/reaction/delete.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 87c855a5e8..28be360763 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,10 +1,10 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { @@ -50,7 +50,7 @@ export default define(meta, paramDef, async (ps, user) => { }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) + .andWhere('note.renoteId = :renoteId', { renoteId: note.id }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 3053eabe33..ab0018f58e 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Notes } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index bb85c92008..777de7221c 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,11 +1,11 @@ +import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; +import { safeForSql } from '@/misc/safe-for-sql.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { Brackets } from 'typeorm'; -import { safeForSql } from '@/misc/safe-for-sql.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index af9b5f0a10..4e2cdae801 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,8 +1,8 @@ -import es from '../../../../db/elasticsearch.js'; -import define from '../../define.js'; -import { Notes } from '@/models/index.js'; import { In } from 'typeorm'; +import { Notes } from '@/models/index.js'; import config from '@/config/index.js'; +import es from '../../../../db/elasticsearch.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; @@ -99,7 +99,7 @@ export default define(meta, paramDef, async (ps, me) => { userHost: ps.host, }, }] : [] - : []; + : []; const result = await es.search({ index: config.elasticsearch.index || 'misskey_note', diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index d6692923c3..5cd74bd2ca 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,7 +1,7 @@ +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Notes } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 069f11fa4a..01afa5add2 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index e48a2cf576..cf360526d3 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,9 +1,9 @@ -import define from '../../../define.js'; -import { getNote } from '../../../common/getters.js'; -import { ApiError } from '../../../error.js'; import { Notes, NoteThreadMutings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import readNote from '@/services/note/read.js'; +import define from '../../../define.js'; +import { getNote } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index 4fb3137a5e..ac310d0fe6 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,7 +1,7 @@ +import { NoteThreadMutings } from '@/models/index.js'; import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; -import { NoteThreadMutings } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 0f976d18be..22f4925175 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,11 +1,10 @@ +import { Brackets } from 'typeorm'; +import { Notes, Followings } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { Notes, Followings } from '@/models/index.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; import { generateRepliesQuery } from '../../common/generate-replies-query.js'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js'; import { generateChannelQuery } from '../../common/generate-channel-query.js'; @@ -62,10 +61,10 @@ export default define(meta, paramDef, async (ps, user) => { .where('following.followerId = :followerId', { followerId: user.id }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere(new Brackets(qb => { qb .where('note.userId = :meId', { meId: user.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); + if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') @@ -84,7 +83,6 @@ export default define(meta, paramDef, async (ps, user) => { generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); - generateMutedInstanceQuery(query, user); generateMutedNoteQuery(query, user); generateBlockedUserQuery(query, user); @@ -126,9 +124,7 @@ export default define(meta, paramDef, async (ps, user) => { const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } + activeUsersChart.read(user); }); return await Notes.packMany(timeline, user); diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 5e8c31eaf8..3fba0efe0c 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,9 +1,9 @@ +import ms from 'ms'; import deleteNote from '@/services/note/delete.js'; +import { Notes, Users } from '@/models/index.js'; import define from '../../define.js'; -import ms from 'ms'; import { getNote } from '../../common/getters.js'; import { ApiError } from '../../error.js'; -import { Notes, Users } from '@/models/index.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index fd4a879035..e603a8f625 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,10 +1,10 @@ +import { Brackets } from 'typeorm'; +import { UserLists, UserListJoinings, Notes } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { UserLists, UserListJoinings, Notes } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { activeUsersChart } from '@/services/chart/index.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'lists'], diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts index 8fdf84624e..7d482b0732 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import watch from '@/services/note/watch.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts index d58f09797c..2c1a2e5fbd 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import unwatch from '@/services/note/unwatch.js'; +import define from '../../../define.js'; import { getNote } from '../../../common/getters.js'; import { ApiError } from '../../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index b339c8723d..80d513d8da 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { createNotification } from '@/services/create-notification.js'; +import define from '../../define.js'; export const meta = { tags: ['notifications'], diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 4575cba43f..d169afbb35 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,7 +1,7 @@ import { publishMainStream } from '@/services/stream.js'; import { pushNotification } from '@/services/push-notification.js'; -import define from '../../define.js'; import { Notifications } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['notifications', 'account'], diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index e7839b2460..7bce525a55 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -2,17 +2,14 @@ import define from '../../define.js'; import { readNotification } from '../../common/read-notification.js'; export const meta = { - desc: { - 'ja-JP': '通知を既読にします。', - 'en-US': 'Mark a notification as read.' - }, - tags: ['notifications', 'account'], requireCredential: true, kind: 'write:notifications', + description: 'Mark a notification as read.', + errors: { noSuchNotification: { message: 'No such notification.', diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 7096aaa3d3..6dd3ede85a 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,6 +1,6 @@ -import define from '../define.js'; import { publishMainStream } from '@/services/stream.js'; import { Users, Pages } from '@/models/index.js'; +import define from '../define.js'; import { ApiError } from '../error.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index c171cd39f5..b008cde84e 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,8 +1,8 @@ import ms from 'ms'; -import define from '../../define.js'; import { Pages, DriveFiles } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Page } from '@/models/entities/page.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -51,7 +51,7 @@ export const paramDef = { } }, script: { type: 'string' }, eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, - font: { type: 'string', enum: ['serif', 'sans-serif'], default: "sans-serif" }, + font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' }, alignCenter: { type: 'boolean', default: false }, hideTitleWhenPinned: { type: 'boolean', default: false }, }, diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index e35ad9ebf2..a7708e6585 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,6 +1,6 @@ +import { Pages } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages } from '@/models/index.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index eeb6d509ca..5a149a626e 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Pages } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 20793db988..269b539f74 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,7 +1,7 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; import { Pages, PageLikes } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 636f3c7149..6b3a2bec10 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,6 +1,6 @@ +import { Pages, PageLikes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages, PageLikes } from '@/models/index.js'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index bf95ab36f2..d241f585aa 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,8 +1,8 @@ import ms from 'ms'; +import { Not } from 'typeorm'; +import { Pages, DriveFiles } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Pages, DriveFiles } from '@/models/index.js'; -import { Not } from 'typeorm'; export const meta = { tags: ['pages'], diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 8d253c1f33..41595b47d9 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,9 +1,9 @@ -import define from '../define.js'; +import { IsNull } from 'typeorm'; import { Users } from '@/models/index.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import * as Acct from '@/misc/acct.js'; import { User } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import define from '../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index cc602857de..c6a940c65e 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,8 +1,8 @@ +import { PromoReads } from '@/models/index.js'; +import { genId } from '@/misc/gen-id.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getNote } from '../../common/getters.js'; -import { PromoReads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 12ce7a9834..511a6bbb53 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,13 +1,13 @@ -import { publishMainStream } from '@/services/stream.js'; -import define from '../define.js'; import rndstr from 'rndstr'; -import config from '@/config/index.js'; import ms from 'ms'; +import { IsNull } from 'typeorm'; +import { publishMainStream } from '@/services/stream.js'; +import config from '@/config/index.js'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; import { sendEmail } from '@/services/send-email.js'; -import { ApiError } from '../error.js'; import { genId } from '@/misc/gen-id.js'; -import { IsNull } from 'typeorm'; +import { ApiError } from '../error.js'; +import define from '../define.js'; export const meta = { tags: ['reset password'], diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 5ff115dab5..140f96d579 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,6 +1,6 @@ +import { resetDb } from '@/db/postgre.js'; import define from '../define.js'; import { ApiError } from '../error.js'; -import { resetDb } from '@/db/postgre.js'; export const meta = { tags: ['non-productive'], diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 3dcb0b9b83..797169c2c3 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import { publishMainStream } from '@/services/stream.js'; -import define from '../define.js'; import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; +import define from '../define.js'; import { ApiError } from '../error.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index f8a1ee29de..cc94f8bf26 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Instances, NoteReactions, Notes, Users } from '@/models/index.js'; +import define from '../define.js'; import { } from '@/services/chart/index.js'; import { IsNull } from 'typeorm'; diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 5bc3b9b6a1..437f8874ff 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,7 +1,7 @@ -import define from '../../define.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { genId } from '@/misc/gen-id.js'; import { SwSubscriptions } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index c21856d28f..c19e06b879 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { SwSubscriptions } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 04b754f4ad..3e41aeaed8 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,6 +1,6 @@ -import define from '../../define.js'; -import { Users, UsedUsernames } from '@/models/index.js'; import { IsNull } from 'typeorm'; +import { Users, UsedUsernames } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 10527d15cc..3a8211374b 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,5 +1,5 @@ -import define from '../define.js'; import { Users } from '@/models/index.js'; +import define from '../define.js'; import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query.js'; import { generateBlockQueryForUsers } from '../common/generate-block-query.js'; @@ -25,8 +25,14 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: "all" }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, + state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, + hostname: { + type: 'string', + nullable: true, + default: null, + description: 'The local host is represented with `null`.', + }, }, required: [], } as const; @@ -48,6 +54,10 @@ export default define(meta, paramDef, async (ps, me) => { case 'remote': query.andWhere('user.host IS NOT NULL'); break; } + if (ps.hostname) { + query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); + } + switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 37d4153950..09fdf27c23 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Clips } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { @@ -32,7 +32,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId) - .andWhere(`clip.userId = :userId`, { userId: ps.userId }) + .andWhere('clip.userId = :userId', { userId: ps.userId }) .andWhere('clip.isPublic = true'); const clips = await query diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index b1fb656208..7f9f980764 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,9 +1,9 @@ +import { IsNull } from 'typeorm'; +import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { toPunyNullable } from '@/misc/convert-host.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { IsNull } from 'typeorm'; export const meta = { tags: ['users'], @@ -96,7 +96,7 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followeeId = :userId`, { userId: user.id }) + .andWhere('following.followeeId = :userId', { userId: user.id }) .innerJoinAndSelect('following.follower', 'follower'); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 429a5e80e5..0aaa810f76 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,9 +1,9 @@ +import { IsNull } from 'typeorm'; +import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { toPunyNullable } from '@/misc/convert-host.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { IsNull } from 'typeorm'; export const meta = { tags: ['users'], @@ -96,7 +96,7 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere(`following.followerId = :userId`, { userId: user.id }) + .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); const followings = await query diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index ab5837b3f3..56965d3066 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,9 +1,9 @@ -import define from '../../define.js'; +import { Not, In, IsNull } from 'typeorm'; import { maximum } from '@/prelude/array.js'; +import { Notes, Users } from '@/models/index.js'; +import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getUser } from '../../common/getters.js'; -import { Not, In, IsNull } from 'typeorm'; -import { Notes, Users } from '@/models/index.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index fcaf4af3c3..4a6362a3c6 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,8 +1,8 @@ -import define from '../../../define.js'; import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index 1bf253ae3f..2ff1f9aec1 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -1,6 +1,6 @@ +import { UserGroups } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups } from '@/models/index.js'; export const meta = { tags: ['groups'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index eafd7f592c..220fff5f3e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,8 +1,8 @@ -import define from '../../../../define.js'; -import { ApiError } from '../../../../error.js'; import { UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; +import { ApiError } from '../../../../error.js'; +import define from '../../../../define.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 08d3a3804b..8d1d3db734 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -1,6 +1,6 @@ +import { UserGroupInvitations } from '@/models/index.js'; import define from '../../../../define.js'; import { ApiError } from '../../../../error.js'; -import { UserGroupInvitations } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index cc82e43f21..1a8d320f3a 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -1,10 +1,10 @@ -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { createNotification } from '@/services/create-notification.js'; +import { getUser } from '../../../common/getters.js'; +import { ApiError } from '../../../error.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index 6a2862ee5a..16c6e544e5 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -1,6 +1,6 @@ -import define from '../../../define.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import { Not, In } from 'typeorm'; +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'account'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 2343cdf857..83dc757db1 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -1,6 +1,6 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index de030193cc..d77cf1a52e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { UserGroups } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['groups', 'account'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index 703dad6d3b..ba67a1e5c9 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,7 +1,7 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index e1cee5fcf7..21e3d9da26 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -1,6 +1,6 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'account'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index 1496e766ca..6456e70dd5 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -1,7 +1,7 @@ +import { UserGroups, UserGroupJoinings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserGroups, UserGroupJoinings } from '@/models/index.js'; export const meta = { tags: ['groups', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index 43cf3e484e..0a96165fc4 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -1,6 +1,6 @@ +import { UserGroups } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserGroups } from '@/models/index.js'; export const meta = { tags: ['groups'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index d2941a0af5..783e63f5de 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,7 +1,7 @@ -import define from '../../../define.js'; import { UserLists } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { UserList } from '@/models/entities/user-list.js'; +import define from '../../../define.js'; export const meta = { tags: ['lists'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index 8cd02ee02a..5a7613c98a 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index b337f879b1..889052fa30 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { UserLists } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['lists', 'account'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index fa7033b02e..d3d1d6555c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,8 +1,8 @@ import { publishUserListStream } from '@/services/stream.js'; +import { UserLists, UserListJoinings, Users } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { UserLists, UserListJoinings, Users } from '@/models/index.js'; export const meta = { tags: ['lists', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 1db10afc80..12b7b86342 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,8 +1,8 @@ +import { pushUserToUserList } from '@/services/user-list/push.js'; +import { UserLists, UserListJoinings, Blockings } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; import { getUser } from '../../../common/getters.js'; -import { pushUserToUserList } from '@/services/user-list/push.js'; -import { UserLists, UserListJoinings, Blockings } from '@/models/index.js'; export const meta = { tags: ['lists', 'users'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 94d24e1274..fd0612f735 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists', 'account'], diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index c21cdcf679..65e708b959 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,6 +1,6 @@ +import { UserLists } from '@/models/index.js'; import define from '../../../define.js'; import { ApiError } from '../../../error.js'; -import { UserLists } from '@/models/index.js'; export const meta = { tags: ['lists'], diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 57dcdfaa88..9fa56fe83a 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,13 +1,12 @@ +import { Brackets } from 'typeorm'; +import { Notes } from '@/models/index.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; import { getUser } from '../../common/getters.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; -import { Notes } from '@/models/index.js'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js'; -import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; -import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; export const meta = { tags: ['users', 'notes'], @@ -77,9 +76,10 @@ export default define(meta, paramDef, async (ps, me) => { .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me, user); - if (me) generateBlockedUserQuery(query, me); - if (me) generateMutedInstanceQuery(query, me); + if (me) { + generateMutedUserQuery(query, me, user); + generateBlockedUserQuery(query, me); + } if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index 85d122c24f..b1d28af845 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Pages } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { @@ -32,7 +32,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere(`page.userId = :userId`, { userId: ps.userId }) + .andWhere('page.userId = :userId', { userId: ps.userId }) .andWhere('page.visibility = \'public\''); const pages = await query diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 64994aae49..9668bd21b8 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { NoteReactions, UserProfiles } from '@/models/index.js'; +import define from '../../define.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { ApiError } from '../../error.js'; @@ -52,8 +52,8 @@ export default define(meta, paramDef, async (ps, me) => { } const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(`reaction.userId = :userId`, { userId: ps.userId }) + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('reaction.userId = :userId', { userId: ps.userId }) .leftJoinAndSelect('reaction.note', 'note'); generateVisibilityQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 6fff94ddcf..e7654e1714 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,6 +1,6 @@ import ms from 'ms'; -import define from '../../define.js'; import { Users, Followings } from '@/models/index.js'; +import define from '../../define.js'; import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query.js'; import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query.js'; diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 87cab5fcf1..233a6a90b4 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Users } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index c7c7a3f591..a9987eafa9 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,12 +1,12 @@ import * as sanitizeHtml from 'sanitize-html'; -import define from '../../define.js'; import { publishAdminStream } from '@/services/stream.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; import { AbuseUserReports, Users } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { sendEmail } from '@/services/send-email.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { getUser } from '../../common/getters.js'; +import { ApiError } from '../../error.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 6cbf12b3b5..6e5bc46bb5 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; -import { Followings, Users } from '@/models/index.js'; import { Brackets } from 'typeorm'; +import { Followings, Users } from '@/models/index.js'; import { USER_ACTIVE_THRESHOLD } from '@/const.js'; import { User } from '@/models/entities/user.js'; +import define from '../../define.js'; export const meta = { tags: ['users'], @@ -67,7 +67,7 @@ export default define(meta, paramDef, async (ps, me) => { const query = Users.createQueryBuilder('user') .where(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere(new Brackets(qb => { qb @@ -85,7 +85,7 @@ export default define(meta, paramDef, async (ps, me) => { if (users.length < ps.limit) { const otherQuery = await Users.createQueryBuilder('user') .where(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere(`user.id != :meId`, { meId: me.id }) + .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.updatedAt IS NOT NULL'); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 19c1a2c690..01729de667 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,7 +1,7 @@ -import define from '../../define.js'; +import { Brackets } from 'typeorm'; import { UserProfiles, Users } from '@/models/index.js'; import { User } from '@/models/entities/user.js'; -import { Brackets } from 'typeorm'; +import define from '../../define.js'; export const meta = { tags: ['users'], @@ -27,7 +27,7 @@ export const paramDef = { query: { type: 'string' }, offset: { type: 'integer', default: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - origin: { type: 'string', enum: ['local', 'remote', 'combined'], default: "combined" }, + origin: { type: 'string', enum: ['local', 'remote', 'combined'], default: 'combined' }, detail: { type: 'boolean', default: true }, }, required: ['query'], @@ -113,7 +113,7 @@ export default define(meta, paramDef, async (ps, me) => { .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .take(ps.limit) .skip(ps.offset) - .getMany() + .getMany(), ); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index b31ca30647..846d83b49f 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,10 +1,10 @@ +import { FindOptionsWhere, In, IsNull } from 'typeorm'; import { resolveUser } from '@/remote/resolve-user.js'; +import { Users } from '@/models/index.js'; +import { User } from '@/models/entities/user.js'; import define from '../../define.js'; import { apiLogger } from '../../logger.js'; import { ApiError } from '../../error.js'; -import { Users } from '@/models/index.js'; -import { FindOptionsWhere, In, IsNull } from 'typeorm'; -import { User } from '@/models/entities/user.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index d17e8b64b5..47f322ee9b 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,7 +1,7 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; import { awaitAll } from '@/prelude/await-all.js'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 02bec31b17..83ece51f51 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -8,6 +8,8 @@ import multer from '@koa/multer'; import bodyParser from 'koa-bodyparser'; import cors from '@koa/cors'; +import { Instances, AccessTokens, Users } from '@/models/index.js'; +import config from '@/config/index.js'; import endpoints from './endpoints.js'; import handler from './api-handler.js'; import signup from './private/signup.js'; @@ -16,8 +18,6 @@ import signupPending from './private/signup-pending.js'; import discord from './service/discord.js'; import github from './service/github.js'; import twitter from './service/twitter.js'; -import { Instances, AccessTokens, Users } from '@/models/index.js'; -import config from '@/config/index.js'; // Init app const app = new Koa(); @@ -56,11 +56,24 @@ for (const endpoint of endpoints) { if (endpoint.meta.requireFile) { router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)); } else { + // 後方互換性のため if (endpoint.name.includes('-')) { - // 後方互換性のため router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); + } else { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; }); + } } + router.post(`/${endpoint.name}`, handler.bind(null, endpoint)); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name}`, handler.bind(null, endpoint)); + } else { + router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; }); + } } } diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index 23430cf8b6..9a7751716e 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -1,12 +1,14 @@ import Limiter from 'ratelimiter'; -import { redisClient } from '../../db/redis.js'; -import { IEndpointMeta } from './endpoints.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; import Logger from '@/services/logger.js'; +import { redisClient } from '../../db/redis.js'; +import { IEndpointMeta } from './endpoints.js'; const logger = new Logger('limiter'); export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => { + if (process.env.NODE_ENV === 'test') ok(); + const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasLongTermLimit = diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 3929fff3f7..68fa814041 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -3,7 +3,7 @@ import config from '@/config/index.js'; import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; -export function genOpenapiSpec(lang = 'ja-JP') { +export function genOpenapiSpec() { const spec = { openapi: '3.0.0', diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index afd14946e1..d28320d928 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,7 +1,6 @@ import Channel from '../channel.js'; import { Notes } from '@/models/index.js'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { StreamMessages } from '../types.js'; export default class extends Channel { @@ -27,9 +26,9 @@ export default class extends Channel { const note = await Notes.pack(data.body.id, this.user, { detail: true }); // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 16ad809395..5148cfd055 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,7 +1,6 @@ import Channel from '../channel.js'; import { Notes, Users } from '@/models/index.js'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { User } from '@/models/entities/user.js'; import { StreamMessages } from '../types.js'; import { Packed } from '@/misc/schema.js'; @@ -45,9 +44,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 1c7e038ab2..5b4ae850ec 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,10 +1,9 @@ -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import Channel from '../channel.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Notes } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; export default class extends Channel { @@ -55,9 +54,9 @@ export default class extends Channel { if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 1b7a58022f..741db447e6 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,8 +1,7 @@ -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import Channel from '../channel.js'; import { Notes } from '@/models/index.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; export default class extends Channel { @@ -38,9 +37,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 3a8e55202a..075a242ef0 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,8 +1,7 @@ -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import Channel from '../channel.js'; import { Notes } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { Packed } from '@/misc/schema.js'; @@ -63,9 +62,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index f3ceeffa1a..f5dedf77ce 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,9 +1,8 @@ -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import Channel from '../channel.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Notes } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { Packed } from '@/misc/schema.js'; @@ -71,9 +70,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 4e198482a0..f01f477238 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,9 +1,8 @@ -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import Channel from '../channel.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Notes } from '@/models/index.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; export default class extends Channel { @@ -52,9 +51,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index d8034e83fe..97ad2983c5 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,8 +1,7 @@ import Channel from '../channel.js'; import { Notes, UserListJoinings, UserLists } from '@/models/index.js'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; import { User } from '@/models/entities/user.js'; -import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { Packed } from '@/misc/schema.js'; export default class extends Channel { @@ -76,9 +75,9 @@ export default class extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isMutedUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.muting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isBlockerUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.blocking)) return; this.send('note', note); } diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts index 48887bf12f..ca036e8fdf 100644 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ b/packages/backend/src/server/proxy/proxy-media.ts @@ -1,13 +1,16 @@ import * as fs from 'node:fs'; import Koa from 'koa'; -import { serverLogger } from '../index.js'; +import sharp from 'sharp'; import { IImage, convertToWebp } from '@/services/drive/image-processor.js'; import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; import { StatusError } from '@/misc/fetch.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { serverLogger } from '../index.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function proxyMedia(ctx: Koa.Context) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; @@ -23,14 +26,50 @@ export async function proxyMedia(ctx: Koa.Context) { await downloadUrl(url, path); const { mime, ext } = await detectType(path); + const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); let image: IImage; - if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) { + if ('static' in ctx.query && isConvertibleImage) { image = await convertToWebp(path, 498, 280); - } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) { + } else if ('preview' in ctx.query && isConvertibleImage) { image = await convertToWebp(path, 200, 200); - } else if (['image/svg+xml'].includes(mime)) { + } else if ('badge' in ctx.query) { + if (!isConvertibleImage) { + // 画像でないなら404でお茶を濁す + throw new StatusError('Unexpected mime', 404); + } + + const mask = sharp(path) + .resize(96, 96, { + fit: 'inside', + withoutEnlargement: false, + }) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .toColorspace('b-w'); + + const stats = await mask.clone().stats(); + + if (stats.entropy < 0.1) { + // エントロピーがあまりない場合は404にする + throw new StatusError('Skip to provide badge', 404); + } + + const data = sharp({ + create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(await mask.png().toBuffer(), 'eor'); + + image = { + data: await data.png().toBuffer(), + ext: 'png', + type: 'image/png', + }; + } else if (mime === 'image/svg+xml') { image = await convertToWebp(path, 2048, 2048, 1); } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); @@ -48,7 +87,7 @@ export async function proxyMedia(ctx: Koa.Context) { } catch (e) { serverLogger.error(`${e}`); - if (e instanceof StatusError && e.isClientError) { + if (e instanceof StatusError && (e.statusCode === 302 || e.isClientError)) { ctx.status = e.statusCode; } else { ctx.status = 500; diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 94329e11c9..0a5cc0e0dc 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -14,10 +14,10 @@ // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので (async () => { window.onerror = (e) => { - renderError('SOMETHING_HAPPENED', e.toString()); + renderError('SOMETHING_HAPPENED', e); }; window.onunhandledrejection = (e) => { - renderError('SOMETHING_HAPPENED_IN_PROMISE', e.toString()); + renderError('SOMETHING_HAPPENED_IN_PROMISE', e); }; const v = localStorage.getItem('v') || VERSION; @@ -57,7 +57,7 @@ import(`/assets/${CLIENT_ENTRY}`) .catch(async e => { await checkUpdate(); - renderError('APP_FETCH_FAILED', JSON.stringify(e)); + renderError('APP_FETCH_FAILED', e); }) //#endregion @@ -102,22 +102,169 @@ document.head.appendChild(style); } - // eslint-disable-next-line no-inner-declarations + async function addStyle(styleText) { + let css = document.createElement('style'); + css.appendChild(document.createTextNode(styleText)); + document.head.appendChild(css); + } + function renderError(code, details) { - document.documentElement.innerHTML = ` - <h1>⚠エラーが発生しました</h1> - <p>問題が解決しない場合は管理者までお問い合わせください。以下のオプションを試すこともできます:</p> - <ul> - <li><a href="/cli">簡易クライアント</a>を起動</li> - <li><a href="/bios">BIOS</a>で修復を試みる</li> - <li><a href="/flush">キャッシュをクリア</a>する</li> - </ul> - <hr> + let errorsElement = document.getElementById('errors'); + + if (!errorsElement) { + document.documentElement.innerHTML = ` + <svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> + <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> + <path d="M12 9v2m0 4v.01"></path> + <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path> + </svg> + <h1>An error has occurred!</h1> + <button class="button-big" onclick="location.reload(true);"> + <span class="button-label-big">Refresh</span> + </button> + <p class="dont-worry">Don't worry, it's (probably) not your fault.</p> + <p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p> + <a href="/flush"> + <button class="button-small"> + <span class="button-label-small">Clear preferences and cache</span> + </button> + </a> + <br> + <a href="/cli"> + <button class="button-small"> + <span class="button-label-small">Start the simple client</span> + </button> + </a> + <br> + <a href="/bios"> + <button class="button-small"> + <span class="button-label-small">Start the repair tool</span> + </button> + </a> + <br> + <div id="errors"></div> + `; + errorsElement = document.getElementById('errors'); + } + const detailsElement = document.createElement('details'); + detailsElement.innerHTML = ` + <br> + <summary> <code>ERROR CODE: ${code}</code> - <details> - ${details} - </details> - `; + </summary> + <code>${JSON.stringify(details)}</code>`; + errorsElement.appendChild(detailsElement); + addStyle(` + * { + font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; + } + + #misskey_app, + #splash { + display: none !important; + } + + body, + html { + background-color: #222; + color: #dfddcc; + justify-content: center; + margin: auto; + padding: 10px; + text-align: center; + } + + button { + border-radius: 999px; + padding: 0px 12px 0px 12px; + border: none; + cursor: pointer; + margin-bottom: 12px; + } + + .button-big { + background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); + line-height: 50px; + } + + .button-big:hover { + background: rgb(153, 204, 0); + } + + .button-small { + background: #444; + line-height: 40px; + } + + .button-small:hover { + background: #555; + } + + .button-label-big { + color: #222; + font-weight: bold; + font-size: 20px; + padding: 12px; + } + + .button-label-small { + color: rgb(153, 204, 0); + font-size: 16px; + padding: 12px; + } + + a { + color: rgb(134, 179, 0); + text-decoration: none; + } + + p, + li { + font-size: 16px; + } + + .dont-worry, + #msg { + font-size: 18px; + } + + .icon-warning { + color: #dec340; + height: 4rem; + padding-top: 2rem; + } + + h1 { + font-size: 32px; + } + + code { + font-family: Fira, FiraCode, monospace; + } + + details { + background: #333; + margin-bottom: 2rem; + padding: 0.5rem 1rem; + width: 40rem; + border-radius: 10px; + justify-content: center; + margin: auto; + } + + summary { + cursor: pointer; + } + + summary > * { + display: inline; + } + + @media screen and (max-width: 500px) { + details { + width: 50%; + } + `) } // eslint-disable-next-line no-inner-declarations diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 2feee72be7..be95becb68 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -11,6 +11,7 @@ import Router from '@koa/router'; import send from 'koa-send'; import favicon from 'koa-favicon'; import views from 'koa-views'; +import sharp from 'sharp'; import { createBullBoard } from '@bull-board/api'; import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { KoaAdapter } from '@bull-board/koa'; @@ -140,6 +141,49 @@ router.get('/twemoji/(.*)', async ctx => { }); }); +router.get('/twemoji-badge/(.*)', async ctx => { + const path = ctx.path.replace('/twemoji-badge/', ''); + + if (!path.match(/^[0-9a-f-]+\.png$/)) { + ctx.status = 404; + return; + } + + const mask = await sharp( + `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`, + { density: 1000 }, + ) + .resize(488, 488) + .greyscale() + .normalise() + .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast + .flatten({ background: '#000' }) + .extend({ + top: 12, + bottom: 12, + left: 12, + right: 12, + background: '#000', + }) + .toColorspace('b-w') + .png() + .toBuffer(); + + const buffer = await sharp({ + create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, + }) + .pipelineColorspace('b-w') + .boolean(mask, 'eor') + .resize(96, 96) + .png() + .toBuffer(); + + ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + ctx.set('Cache-Control', 'max-age=2592000'); + ctx.set('Content-Type', 'image/png'); + ctx.body = buffer; +}); + // ServiceWorker router.get(`/sw.js`, async ctx => { await send(ctx as any, `/sw.js`, { diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug index d81a3ee67f..39a151a29b 100644 --- a/packages/backend/src/server/web/views/bios.pug +++ b/packages/backend/src/server/web/views/bios.pug @@ -5,7 +5,7 @@ html head meta(charset='utf-8') meta(name='application-name' content='Misskey') - title Misskey BIOS + title Misskey Repair Tool style include ../bios.css script @@ -13,7 +13,7 @@ html body header - h1 Misskey BIOS #{version} + h1 Misskey Repair Tool #{version} main div.tabs button#ls edit local storage diff --git a/packages/backend/src/services/add-note-to-antenna.ts b/packages/backend/src/services/add-note-to-antenna.ts index f86f394f80..1f344222e1 100644 --- a/packages/backend/src/services/add-note-to-antenna.ts +++ b/packages/backend/src/services/add-note-to-antenna.ts @@ -2,7 +2,7 @@ import { Antenna } from '@/models/entities/antenna.js'; import { Note } from '@/models/entities/note.js'; import { AntennaNotes, Mutings, Notes } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; -import { isMutedUserRelated } from '@/misc/is-muted-user-related.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { publishAntennaStream, publishMainStream } from '@/services/stream.js'; import { User } from '@/models/entities/user.js'; @@ -39,7 +39,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { _note.renote = await Notes.findOneByOrFail({ id: note.renoteId }); } - if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) { + if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) { return; } diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts new file mode 100644 index 0000000000..0fdceb671b --- /dev/null +++ b/packages/backend/src/services/delete-account.ts @@ -0,0 +1,23 @@ +import { Users } from '@/models/index.js'; +import { createDeleteAccountJob } from '@/queue/index.js'; +import { publishUserEvent } from './stream.js'; +import { doPostSuspend } from './suspend-user.js'; + +export async function deleteAccount(user: { + id: string; + host: string | null; +}): Promise<void> { + // 物理削除する前にDelete activityを送信する + await doPostSuspend(user).catch(e => {}); + + createDeleteAccountJob(user, { + soft: false, + }); + + await Users.update(user.id, { + isDeleted: true, + }); + + // Terminate streaming + publishUserEvent(user.id, 'terminate', {}); +} diff --git a/packages/backend/src/services/detect-sensitive.ts b/packages/backend/src/services/detect-sensitive.ts new file mode 100644 index 0000000000..0fa263599b --- /dev/null +++ b/packages/backend/src/services/detect-sensitive.ts @@ -0,0 +1,28 @@ +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as nsfw from 'nsfwjs'; +import * as tf from '@tensorflow/tfjs-node'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +let model: nsfw.NSFWJS; + +export async function detectSensitive(path: string): Promise<nsfw.predictionType[] | null> { + try { + if (model == null) model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); + + const buffer = await fs.promises.readFile(path); + const image = await tf.node.decodeImage(buffer, 3) as tf.Tensor3D; + try { + const predictions = await model.classify(image); + return predictions; + } finally { + image.dispose(); + } + } catch (err) { + console.error(err); + return null; + } +} diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index cfbcb60ddf..709db88f2f 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -2,26 +2,27 @@ import * as fs from 'node:fs'; import { v4 as uuid } from 'uuid'; +import S3 from 'aws-sdk/clients/s3.js'; +import sharp from 'sharp'; +import { IsNull } from 'typeorm'; import { publishMainStream, publishDriveStream } from '@/services/stream.js'; -import { deleteFile } from './delete-file.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; -import { driveLogger } from './logger.js'; -import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { getFileInfo } from '@/misc/get-file-info.js'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; -import { InternalStorage } from './internal-storage.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { IRemoteUser, User } from '@/models/entities/user.js'; import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js'; import { genId } from '@/misc/gen-id.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import S3 from 'aws-sdk/clients/s3.js'; -import { getS3 } from './s3.js'; -import sharp from 'sharp'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; -import { IsNull } from 'typeorm'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { getS3 } from './s3.js'; +import { InternalStorage } from './internal-storage.js'; +import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; +import { driveLogger } from './logger.js'; +import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; +import { deleteFile } from './delete-file.js'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -171,7 +172,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool } if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) { - logger.debug(`web image and thumbnail not created (not an required file)`); + logger.debug('web image and thumbnail not created (not an required file)'); return { webpublic: null, thumbnail: null, @@ -212,7 +213,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool let webpublic: IImage | null = null; if (generateWeb && !satisfyWebpublic) { - logger.info(`creating web image`); + logger.info('creating web image'); try { if (['image/jpeg', 'image/webp'].includes(type)) { @@ -222,14 +223,14 @@ export async function generateAlts(path: string, type: string, generateWeb: bool } else if (['image/svg+xml'].includes(type)) { webpublic = await convertSharpToPng(img, 2048, 2048); } else { - logger.debug(`web image not created (not an required image)`); + logger.debug('web image not created (not an required image)'); } } catch (err) { - logger.warn(`web image not created (an error occured)`, err as Error); + logger.warn('web image not created (an error occured)', err as Error); } } else { - if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`); - else logger.info(`web image not created (from remote)`); + if (satisfyWebpublic) logger.info('web image not created (original satisfies webpublic)'); + else logger.info('web image not created (from remote)'); } // #endregion webpublic @@ -240,10 +241,10 @@ export async function generateAlts(path: string, type: string, generateWeb: bool if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { thumbnail = await convertSharpToWebp(img, 498, 280); } else { - logger.debug(`thumbnail not created (not an required file)`); + logger.debug('thumbnail not created (not an required file)'); } } catch (err) { - logger.warn(`thumbnail not created (an error occured)`, err as Error); + logger.warn('thumbnail not created (an error occured)', err as Error); } // #endregion thumbnail @@ -276,7 +277,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, const s3 = getS3(meta); const upload = s3.upload(params, { - partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, + partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, }); const result = await upload.promise(); @@ -307,7 +308,7 @@ async function deleteOldFile(user: IRemoteUser) { type AddFileArgs = { /** User who wish to add file */ - user: { id: User['id']; host: User['host'] } | null; + user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; /** File path */ path: string; /** Name */ @@ -326,6 +327,9 @@ type AddFileArgs = { uri?: string | null; /** Mark file as sensitive */ sensitive?: boolean | null; + + requestIp?: string | null; + requestHeaders?: Record<string, string> | null; }; /** @@ -342,11 +346,35 @@ export async function addFile({ isLink = false, url = null, uri = null, - sensitive = null + sensitive = null, + requestIp = null, + requestHeaders = null, }: AddFileArgs): Promise<DriveFile> { - const info = await getFileInfo(path); + let skipNsfwCheck = false; + const instance = await fetchMeta(); + if (user == null) skipNsfwCheck = true; + if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'local' && Users.isRemoteUser(user)) skipNsfwCheck = true; + if (user && instance.sensitiveMediaDetection === 'remote' && Users.isLocalUser(user)) skipNsfwCheck = true; + + const info = await getFileInfo(path, { + skipSensitiveDetection: skipNsfwCheck, + sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる + instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + 0.5, + sensitiveThresholdForPorn: 0.75, + enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + }); logger.info(`${JSON.stringify(info)}`); + // 現状 false positive が多すぎて実用に耐えない + //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { + // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); + //} + // detect name const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); @@ -366,16 +394,23 @@ export async function addFile({ //#region Check drive usage if (user && !isLink) { const usage = await DriveFiles.calcDriveUsageOf(user); + const u = await Users.findOneBy({ id: user.id }); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + + if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { + driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; + logger.debug('drive capacity override applied'); + logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + } logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + info.size > driveCapacity) { if (Users.isLocalUser(user)) { - throw new Error('no-free-space'); + throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); } else { // (アバターまたはバナーを含まず)最も古いファイルを削除する deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); @@ -427,13 +462,20 @@ export async function addFile({ file.properties = properties; file.blurhash = info.blurhash || null; file.isLink = isLink; + file.requestIp = requestIp; + file.requestHeaders = requestHeaders; + file.maybeSensitive = info.sensitive; + file.maybePorn = info.porn; file.isSensitive = user ? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false : false; + if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; + if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; + if (url !== null) { file.src = url; diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index b3fd025abc..6e6666481d 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -1,12 +1,10 @@ import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { createTemp } from '@/misc/create-temp.js'; +import { createTempDir } from '@/misc/create-temp.js'; import { IImage, convertToJpeg } from './image-processor.js'; import FFmpeg from 'fluent-ffmpeg'; export async function GenerateVideoThumbnail(source: string): Promise<IImage> { - const [file, cleanup] = await createTemp(); - const parsed = path.parse(file); + const [dir, cleanup] = await createTempDir(); try { await new Promise((res, rej) => { @@ -16,15 +14,15 @@ export async function GenerateVideoThumbnail(source: string): Promise<IImage> { .on('end', res) .on('error', rej) .screenshot({ - folder: parsed.dir, - filename: parsed.base, + folder: dir, + filename: 'out.png', // must have .png extension count: 1, timestamps: ['5%'], }); }); // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - return await convertToJpeg(file, 498, 280); + return await convertToJpeg(`${dir}/out.png`, 498, 280); } finally { cleanup(); } diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 001fc49ee4..3c5e1aa5c1 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -1,12 +1,12 @@ import { URL } from 'node:url'; -import { addFile } from './add-file.js'; import { User } from '@/models/entities/user.js'; -import { driveLogger } from './logger.js'; import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { DriveFolder } from '@/models/entities/drive-folder.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles } from '@/models/index.js'; +import { driveLogger } from './logger.js'; +import { addFile } from './add-file.js'; const logger = driveLogger.createSubLogger('downloader'); @@ -19,6 +19,8 @@ type Args = { force?: boolean; isLink?: boolean; comment?: string | null; + requestIp?: string | null; + requestHeaders?: Record<string, string> | null; }; export async function uploadFromUrl({ @@ -30,6 +32,8 @@ export async function uploadFromUrl({ force = false, isLink = false, comment = null, + requestIp = null, + requestHeaders = null, }: Args): Promise<DriveFile> { let name = new URL(url).pathname.split('/').pop() || null; if (name == null || !DriveFiles.validateFileName(name)) { @@ -49,7 +53,7 @@ export async function uploadFromUrl({ // write content at URL to temp file await downloadUrl(url, path); - const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); + const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); logger.succ(`Got: ${driveFile.id}`); return driveFile!; } catch (e) { diff --git a/packages/backend/src/services/note/polls/update.ts b/packages/backend/src/services/note/polls/update.ts index 43ca3eff4d..68cbb9835a 100644 --- a/packages/backend/src/services/note/polls/update.ts +++ b/packages/backend/src/services/note/polls/update.ts @@ -14,7 +14,6 @@ export async function deliverQuestionUpdate(noteId: Note['id']) { if (user == null) throw new Error('note not found'); if (Users.isLocalUser(user)) { - const content = renderActivity(renderUpdate(await renderNote(note, false), user)); deliverToFollowers(user, content); deliverToRelays(user, content); |