diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-05-28 01:31:23 +0900 |
|---|---|---|
| committer | tamaina <tamaina@hotmail.co.jp> | 2022-05-28 01:31:23 +0900 |
| commit | fa99d9c6fee3a7d6f72e254e0aa55972cd6538fb (patch) | |
| tree | 43d941bbacc9cfaa911fc785fce0b34af4ef6fcc /packages/backend/src | |
| parent | Merge branch 'develop' into pizzax-indexeddb (diff) | |
| parent | fix(docs): correct information for drive upload (#8736) (diff) | |
| download | sharkey-fa99d9c6fee3a7d6f72e254e0aa55972cd6538fb.tar.gz sharkey-fa99d9c6fee3a7d6f72e254e0aa55972cd6538fb.tar.bz2 sharkey-fa99d9c6fee3a7d6f72e254e0aa55972cd6538fb.zip | |
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/backend/src')
52 files changed, 501 insertions, 473 deletions
diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index 0426cb8bcd..d1f9cd9552 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,4 +1,4 @@ -declare module 'http-signature' { +declare module '@peertube/http-signature' { import { IncomingMessage, ClientRequest } from 'node:http'; interface ISignature { diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 09d20f9361..bf51960482 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -5,7 +5,6 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import * as portscanner from 'portscanner'; import semver from 'semver'; import Logger from '@/services/logger.js'; @@ -48,11 +47,6 @@ function greet() { bootLogger.info(`Misskey v${meta.version}`, null, true); } -function isRoot() { - // maybe process.getuid will be undefined under not POSIX environment (e.g. Windows) - return process.getuid != null && process.getuid() === 0; -} - /** * Init master process */ @@ -67,7 +61,6 @@ export async function masterMain() { showNodejsVersion(); config = loadConfigBoot(); await connectDb(); - await validatePort(config); } catch (e) { bootLogger.error('Fatal error occurred during initialization', null, true); process.exit(1); @@ -97,8 +90,6 @@ function showEnvironment(): void { logger.warn('The environment is not in production mode.'); logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); } - - logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); } function showNodejsVersion(): void { @@ -152,29 +143,6 @@ async function connectDb(): Promise<void> { } } -async function validatePort(config: Config): Promise<void> { - const isWellKnownPort = (port: number) => port < 1024; - - async function isPortAvailable(port: number): Promise<boolean> { - return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; - } - - if (config.port == null || Number.isNaN(config.port)) { - bootLogger.error('The port is not configured. Please configure port.', null, true); - process.exit(1); - } - - if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { - bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); - process.exit(1); - } - - if (!await isPortAvailable(config.port)) { - bootLogger.error(`Port ${config.port} is already in use`, null, true); - process.exit(1); - } -} - async function spawnWorkers(limit: number = 1) { const workers = Math.min(limit, os.cpus().length); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); @@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> { return new Promise(res => { const worker = cluster.fork(); worker.on('message', message => { + if (message === 'listenFailed') { + bootLogger.error(`The server Listen failed due to the previous error.`); + process.exit(1); + } if (message !== 'ready') return; res(); }); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 7f765463e4..c2e6bea45e 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -25,6 +25,7 @@ const path = process.env.NODE_ENV === 'test' export default function load() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); + const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -45,6 +46,7 @@ export default function load() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; + mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, ''); if (!config.redis.prefix) config.redis.prefix = mixin.host; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 58a27803cb..948545db7a 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -80,6 +80,7 @@ export type Mixin = { authUrl: string; driveUrl: string; userAgent: string; + clientEntry: string; }; export type Config = Source & Mixin; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index eb5fc2e186..e09e93f04e 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number); import { Logger, DataSource } from 'typeorm'; import * as highlight from 'cli-highlight'; import config from '@/config/index.js'; -import { envOption } from '../env.js'; - -import { dbLogger } from './logger.js'; import { User } from '@/models/entities/user.js'; import { DriveFile } from '@/models/entities/drive-file.js'; @@ -74,6 +71,8 @@ import { UserPending } from '@/models/entities/user-pending.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'; const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); @@ -212,7 +211,7 @@ export async function initDb() { if (db.isInitialized) { // nop } else { - await db.connect(); + await db.initialize(); } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 01bbe98a85..e5b911ed32 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -48,6 +48,7 @@ export class Cache<T> { // Cache MISS const value = await fetcher(); + this.set(key, value); return value; } diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 04604cf7d0..f07be634fb 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -1,10 +1,19 @@ import * as tmp from 'tmp'; -export function createTemp(): Promise<[string, any]> { - return new Promise<[string, any]>((res, rej) => { +export function createTemp(): Promise<[string, () => void]> { + return new Promise<[string, () => void]>((res, rej) => { tmp.file((e, path, fd, cleanup) => { if (e) return rej(e); res([path, cleanup]); }); }); } + +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]); + }); + }); +} diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index 5417c10962..e855ac28ee 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -20,9 +20,16 @@ export async function fetchMeta(noCache = false): Promise<Meta> { cache = meta; return meta; } else { - const saved = await transactionalEntityManager.save(Meta, { - id: 'x', - }) as Meta; + // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う + const saved = await transactionalEntityManager + .upsert( + Meta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); cache = saved; return saved; diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts index 69cdc49cec..c6e2141a46 100644 --- a/packages/backend/src/models/entities/access-token.ts +++ b/packages/backend/src/models/entities/access-token.ts @@ -15,7 +15,6 @@ export class AccessToken { @Column('timestamp with time zone', { nullable: true, - default: null, }) public lastUsedAt: Date | null; @@ -29,7 +28,6 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public session: string | null; @@ -52,7 +50,6 @@ export class AccessToken { @Column({ ...id(), nullable: true, - default: null, }) public appId: App['id'] | null; @@ -65,21 +62,18 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public name: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public description: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public iconUrl: string | null; diff --git a/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts index b825856201..295d1b486c 100644 --- a/packages/backend/src/models/entities/auth-session.ts +++ b/packages/backend/src/models/entities/auth-session.ts @@ -23,7 +23,7 @@ export class AuthSession { ...id(), nullable: true, }) - public userId: User['id']; + public userId: User['id'] | null; @ManyToOne(type => User, { onDelete: 'CASCADE', diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts index da6b3c7a7f..1386684c32 100644 --- a/packages/backend/src/models/entities/clip.ts +++ b/packages/backend/src/models/entities/clip.ts @@ -37,7 +37,7 @@ export class Clip { public isPublic: boolean; @Column('varchar', { - length: 2048, nullable: true, default: null, + length: 2048, nullable: true, comment: 'The description of the Clip.', }) public description: string | null; diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 3d375f0e35..a636d1d519 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -79,7 +79,6 @@ export class DriveFile { }) public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; - @Index() @Column('boolean') public storedInternal: boolean; diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts index b72ca72331..7332dd1857 100644 --- a/packages/backend/src/models/entities/emoji.ts +++ b/packages/backend/src/models/entities/emoji.ts @@ -36,6 +36,7 @@ export class Emoji { @Column('varchar', { length: 512, + default: '', }) public publicUrl: string; diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts index bb24d6b30f..7ea9234384 100644 --- a/packages/backend/src/models/entities/instance.ts +++ b/packages/backend/src/models/entities/instance.ts @@ -107,53 +107,53 @@ export class Instance { public isSuspended: boolean; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, comment: 'The software of the Instance.', }) public softwareName: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public softwareVersion: string | null; @Column('boolean', { - nullable: true, default: null, + nullable: true, }) public openRegistrations: boolean | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public name: string | null; @Column('varchar', { - length: 4096, nullable: true, default: null, + length: 4096, nullable: true, }) public description: string | null; @Column('varchar', { - length: 128, nullable: true, default: null, + length: 128, nullable: true, }) public maintainerName: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public maintainerEmail: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public iconUrl: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public faviconUrl: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public themeColor: string | null; diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 4d58b5f04f..80b5228bcd 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -78,7 +78,7 @@ export class Meta { public blockedHosts: string[]; @Column('varchar', { - length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}', + length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', }) public pinnedPages: string[]; @@ -346,14 +346,12 @@ export class Meta { @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultLightTheme: string | null; @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultDarkTheme: string | null; diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts index b3a7e7a671..8f9e69063a 100644 --- a/packages/backend/src/models/entities/muting.ts +++ b/packages/backend/src/models/entities/muting.ts @@ -17,7 +17,6 @@ export class Muting { @Index() @Column('timestamp with time zone', { nullable: true, - default: null, }) public expiresAt: Date | null; diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index da49d53b69..0ffeb85f69 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -53,8 +53,8 @@ export class Note { }) public threadId: string | null; - @Column('varchar', { - length: 8192, nullable: true, + @Column('text', { + nullable: true, }) public text: string | null; @@ -179,7 +179,7 @@ export class Note { @Index() @Column({ ...id(), - nullable: true, default: null, + nullable: true, comment: 'The ID of source channel.', }) public channelId: Channel['id'] | null; diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index f95cb144c5..1778742ea2 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -192,6 +192,7 @@ export class UserProfile { @Column('jsonb', { default: [], + comment: 'List of instances muted by the user.', }) public mutedInstances: string[]; diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 29d9a0c2ca..df92fb8259 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -207,7 +207,7 @@ export class User { @Column('boolean', { default: false, - comment: 'Whether to show users replying to other users in the timeline', + comment: 'Whether to show users replying to other users in the timeline.', }) public showTimelineReplies: boolean; diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index cf5fcb1787..638d78f626 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -168,16 +168,22 @@ export const NoteRepository = db.getRepository(Note).extend({ return true; } else { // フォロワーかどうか - const following = await Followings.findOneBy({ - followeeId: note.userId, - followerId: meId, - }); + const [following, user] = await Promise.all([ + Followings.findOneBy({ + followeeId: note.userId, + followerId: meId, + }), + Users.findOneByOrFail({ id: meId }), + ]); - if (following == null) { - return false; - } else { - return true; - } + /* If we know the following, everyhting is fine. + + But if we do not know the following, it might be that both the + author of the note and the author of the like are remote users, + in which case we can never know the following. Instead we have + to assume that the users are following each other. + */ + return following != null || (note.userHost != null && user.host != null); } } diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 2d40290e4c..67d5f5d248 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,4 +1,4 @@ -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import { v4 as uuid } from 'uuid'; import config from '@/config/index.js'; diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts index 166c9e4cd3..f5e0424a79 100644 --- a/packages/backend/src/queue/processors/db/export-blocking.ts +++ b/packages/backend/src/queue/processors/db/export-blocking.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Blockings } from '@/models/index.js'; import { MoreThan } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let exportedCount = 0; - let cursor: any = null; + let exportedCount = 0; + let cursor: any = null; - while (true) { - const blockings = await Blockings.find({ - where: { - blockerId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); + while (true) { + const blockings = await Blockings.find({ + where: { + blockerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); - if (blockings.length === 0) { - job.progress(100); - break; - } + if (blockings.length === 0) { + job.progress(100); + break; + } - cursor = blockings[blockings.length - 1].id; + cursor = blockings[blockings.length - 1].id; - for (const block of blockings) { - const u = await Users.findOneBy({ id: block.blockeeId }); - if (u == null) { - exportedCount++; continue; - } + for (const block of blockings) { + const u = await Users.findOneBy({ id: block.blockeeId }); + if (u == null) { + exportedCount++; continue; + } - const content = getFullApAccount(u.username, u.host); - await new Promise<void>((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + const content = getFullApAccount(u.username, u.host); + await new Promise<void>((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); + exportedCount++; + } + + const total = await Blockings.countBy({ + blockerId: user.id, }); - exportedCount++; - } - const total = await Blockings.countBy({ - blockerId: user.id, - }); + job.progress(exportedCount / total); + } - job.progress(exportedCount / total); - } + stream.end(); + logger.succ(`Exported to: ${path}`); - stream.end(); - logger.succ(`Exported to: ${path}`); + const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); - const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index c2467fb5f0..97ba62dcf6 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -1,5 +1,4 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { ulid } from 'ulid'; @@ -10,6 +9,7 @@ import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { Users, Emojis } from '@/models/index.js'; import { } from '@/queue/types.js'; +import { createTempDir } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import config from '@/config/index.js'; import { IsNull } from 'typeorm'; @@ -25,13 +25,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi return; } - // Create temp dir - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTempDir(); logger.info(`Temp dir is ${path}`); @@ -98,12 +92,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi metaStream.end(); // Create archive - const [archivePath, archiveCleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [archivePath, archiveCleanup] = await createTemp(); const archiveStream = fs.createWriteStream(archivePath); const archive = archiver('zip', { zlib: { level: 0 }, diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts index 965500ac27..4ac165567b 100644 --- a/packages/backend/src/queue/processors/db/export-following.ts +++ b/packages/backend/src/queue/processors/db/export-following.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Followings, Mutings } from '@/models/index.js'; import { In, MoreThan, Not } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () => } // Create temp file - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let cursor: Following['id'] | null = null; + let cursor: Following['id'] | null = null; - const mutings = job.data.excludeMuting ? await Mutings.findBy({ - muterId: user.id, - }) : []; + const mutings = job.data.excludeMuting ? await Mutings.findBy({ + muterId: user.id, + }) : []; - while (true) { - const followings = await Followings.find({ - where: { - followerId: user.id, - ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Following[]; + while (true) { + const followings = await Followings.find({ + where: { + followerId: user.id, + ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Following[]; - if (followings.length === 0) { - break; - } + if (followings.length === 0) { + break; + } - cursor = followings[followings.length - 1].id; + cursor = followings[followings.length - 1].id; - for (const following of followings) { - const u = await Users.findOneBy({ id: following.followeeId }); - if (u == null) { - continue; - } + for (const following of followings) { + const u = await Users.findOneBy({ id: following.followeeId }); + if (u == null) { + continue; + } - if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { - continue; - } + if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { + continue; + } - const content = getFullApAccount(u.username, u.host); - await new Promise<void>((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + const content = getFullApAccount(u.username, u.host); + await new Promise<void>((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); - }); + } } - } - stream.end(); - logger.succ(`Exported to: ${path}`); + stream.end(); + logger.succ(`Exported to: ${path}`); - const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); + const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts index 0ef81971f1..6a36cfa072 100644 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ b/packages/backend/src/queue/processors/db/export-mute.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Mutings } from '@/models/index.js'; import { IsNull, MoreThan } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let exportedCount = 0; - let cursor: any = null; + let exportedCount = 0; + let cursor: any = null; - while (true) { - const mutes = await Mutings.find({ - where: { - muterId: user.id, - expiresAt: IsNull(), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); + while (true) { + const mutes = await Mutings.find({ + where: { + muterId: user.id, + expiresAt: IsNull(), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); - if (mutes.length === 0) { - job.progress(100); - break; - } + if (mutes.length === 0) { + job.progress(100); + break; + } - cursor = mutes[mutes.length - 1].id; + cursor = mutes[mutes.length - 1].id; - for (const mute of mutes) { - const u = await Users.findOneBy({ id: mute.muteeId }); - if (u == null) { - exportedCount++; continue; - } + for (const mute of mutes) { + const u = await Users.findOneBy({ id: mute.muteeId }); + if (u == null) { + exportedCount++; continue; + } - const content = getFullApAccount(u.username, u.host); - await new Promise<void>((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + const content = getFullApAccount(u.username, u.host); + await new Promise<void>((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); + exportedCount++; + } + + const total = await Mutings.countBy({ + muterId: user.id, }); - exportedCount++; - } - const total = await Mutings.countBy({ - muterId: user.id, - }); + job.progress(exportedCount / total); + } - job.progress(exportedCount / total); - } + stream.end(); + logger.succ(`Exported to: ${path}`); - stream.end(); - logger.succ(`Exported to: ${path}`); + const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); - const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts index 7e12a6fac2..051fcdf385 100644 --- a/packages/backend/src/queue/processors/db/export-notes.ts +++ b/packages/backend/src/queue/processors/db/export-notes.ts @@ -1,5 +1,4 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; @@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm'; import { Note } from '@/models/entities/note.js'; import { Poll } from '@/models/entities/poll.js'; import { DbUserJobData } from '@/queue/types.js'; +import { createTemp } from '@/misc/create-temp.js'; const logger = queueLogger.createSubLogger('export-notes'); @@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - const write = (text: string): Promise<void> => { - return new Promise<void>((res, rej) => { - stream.write(text, err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + const write = (text: string): Promise<void> => { + return new Promise<void>((res, rej) => { + stream.write(text, err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); - }); - }; + }; - await write('['); + await write('['); - let exportedNotesCount = 0; - let cursor: Note['id'] | null = null; + let exportedNotesCount = 0; + let cursor: Note['id'] | null = null; - while (true) { - const notes = await Notes.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Note[]; + while (true) { + const notes = await Notes.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Note[]; - if (notes.length === 0) { - job.progress(100); - break; - } + if (notes.length === 0) { + job.progress(100); + break; + } - cursor = notes[notes.length - 1].id; + cursor = notes[notes.length - 1].id; - for (const note of notes) { - let poll: Poll | undefined; - if (note.hasPoll) { - poll = await Polls.findOneByOrFail({ noteId: note.id }); + for (const note of notes) { + let poll: Poll | undefined; + if (note.hasPoll) { + poll = await Polls.findOneByOrFail({ noteId: note.id }); + } + const content = JSON.stringify(serialize(note, poll)); + const isFirst = exportedNotesCount === 0; + await write(isFirst ? content : ',\n' + content); + exportedNotesCount++; } - const content = JSON.stringify(serialize(note, poll)); - const isFirst = exportedNotesCount === 0; - await write(isFirst ? content : ',\n' + content); - exportedNotesCount++; - } - const total = await Notes.countBy({ - userId: user.id, - }); + const total = await Notes.countBy({ + userId: user.id, + }); - job.progress(exportedNotesCount / total); - } + job.progress(exportedNotesCount / total); + } + + await write(']'); - await write(']'); + stream.end(); + logger.succ(`Exported to: ${path}`); - stream.end(); - logger.succ(`Exported to: ${path}`); + const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); - const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts index 45852a6038..71dd72df27 100644 --- a/packages/backend/src/queue/processors/db/export-user-lists.ts +++ b/packages/backend/src/queue/processors/db/export-user-lists.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, UserLists, UserListJoinings } from '@/models/index.js'; import { In } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): }); // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - for (const list of lists) { - const joinings = await UserListJoinings.findBy({ userListId: list.id }); - const users = await Users.findBy({ - id: In(joinings.map(j => j.userId)), - }); + for (const list of lists) { + const joinings = await UserListJoinings.findBy({ userListId: list.id }); + const users = await Users.findBy({ + id: In(joinings.map(j => j.userId)), + }); - for (const u of users) { - const acct = getFullApAccount(u.username, u.host); - const content = `${list.name},${acct}`; - await new Promise<void>((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + for (const u of users) { + const acct = getFullApAccount(u.username, u.host); + const content = `${list.name},${acct}`; + await new Promise<void>((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); - }); + } } - } - stream.end(); - logger.succ(`Exported to: ${path}`); + stream.end(); + logger.succ(`Exported to: ${path}`); + + const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); - const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts index 28e0b867a4..64dfe85374 100644 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -1,9 +1,9 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import unzipper from 'unzipper'; import { queueLogger } from '../../logger.js'; +import { createTempDir } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { DriveFiles, Emojis } from '@/models/index.js'; import { DbUserImportJobData } from '@/queue/types.js'; @@ -25,13 +25,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don return; } - // Create temp dir - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTempDir(); logger.info(`Temp dir is ${path}`); diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 4fbfdb234f..198dde6050 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -1,6 +1,6 @@ import { URL } from 'node:url'; import Bull from 'bull'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import perform from '@/remote/activitypub/perform.js'; import Logger from '@/services/logger.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 6c0b9d9bf6..5ea4725561 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -3,7 +3,7 @@ import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user.js'; import { Webhook } from '@/models/entities/webhook'; import { IActivity } from '@/remote/activitypub/type.js'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; export type DeliverJobData = { /** Actor */ diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index 680749f4d8..759cb4ae83 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { getApLock } from '@/misc/app-lock.js'; import { parseAudience } from '../../audience.js'; import { StatusError } from '@/misc/fetch.js'; +import { Notes } from '@/models/index.js'; const logger = apLogger; @@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac throw e; } + if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity'; + logger.info(`Creating the (Re)Note: ${uri}`); const activityAudience = await parseAudience(actor, activity.to, activity.cc); diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index 4c06a9de0b..c7064f553b 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st } // 削除対象objectのtype - let formarType: string | undefined; + let formerType: string | undefined; if (typeof activity.object === 'string') { // typeが不明だけど、どうせ消えてるのでremote resolveしない - formarType = undefined; + formerType = undefined; } else { const object = activity.object as IObject; if (isTombstone(object)) { - formarType = toSingle(object.formerType); + formerType = toSingle(object.formerType); } else { - formarType = toSingle(object.type); + formerType = toSingle(object.type); } } const uri = getApId(activity.object); // type不明でもactorとobjectが同じならばそれはPersonに違いない - if (!formarType && actor.uri === uri) { - formarType = 'Person'; + if (!formerType && actor.uri === uri) { + formerType = 'Person'; } // それでもなかったらおそらくNote - if (!formarType) { - formarType = 'Note'; + if (!formerType) { + formerType = 'Note'; } - if (validPost.includes(formarType)) { + if (validPost.includes(formerType)) { return await deleteNote(actor, uri); - } else if (validActor.includes(formarType)) { + } else if (validActor.includes(formerType)) { return await deleteActor(actor, uri); } else { - return `Unknown type ${formarType}`; + return `Unknown type ${formerType}`; } }; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts index c2ac31bf8d..417f39722f 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts @@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun const note = await Notes.findOneBy({ uri, + userId: actor.id, }); if (!note) return 'skip: no such Announce'; diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 133dd36066..a48c2d4122 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -1,6 +1,6 @@ import Router from '@koa/router'; import json from 'koa-json-body'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; 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 b23ee9e3df..09e43301b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -27,7 +27,7 @@ export const paramDef = { blockedHosts: { type: 'array', nullable: true, items: { type: 'string', } }, - themeColor: { type: 'string', nullable: true }, + themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, errorImageUrl: { type: 'string', nullable: true }, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index d5e1b19e54..33f5717728 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -2,8 +2,8 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; import config from '@/config/index.js'; -import define from '../../../define.js'; import { UserProfiles } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { requireCredential: true, @@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => { }); // Get the data URL of the authenticator URL - const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({ + const url = speakeasy.otpauthURL({ secret: secret.base32, encoding: 'base32', label: user.username, issuer: config.host, - })); + }); + const dataUrl = await QRCode.toDataURL(url); return { qr: dataUrl, + url, secret: secret.base32, label: user.username, issuer: config.host, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 40a3ba73ca..955f53bbc1 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -172,10 +172,14 @@ export default define(meta, paramDef, async (ps, user) => { let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - files = await DriveFiles.findBy({ - userId: user.id, - id: In(fileIds), - }); + files = await DriveFiles.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: user.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); } let renote: Note | null = null; diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a72a58a843..f93d4f718b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -61,7 +61,14 @@ export default define(meta, paramDef, async (ps, me) => { .getMany(); } else { const nameQuery = Users.createQueryBuilder('user') - .where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + + // Also search username if it qualifies as username + if (Users.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + } + })) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index c6e557aefb..3929fff3f7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') { desc += ` / **Permission**: *${kind}*`; } + const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; + const schema = endpoint.params; + + if (endpoint.meta.requireFile) { + schema.properties.file = { + type: 'string', + format: 'binary', + description: 'The file contents.', + }; + schema.required.push('file'); + } + const info = { operationId: endpoint.name, summary: endpoint.name, @@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') { requestBody: { required: true, content: { - 'application/json': { - schema: endpoint.params, + [requestType]: { + schema, }, }, }, diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 7b66657ad8..0024b8ce3e 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -1,20 +1,25 @@ +import { randomBytes } from 'node:crypto'; import Koa from 'koa'; import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import signin from '../common/signin.js'; +import { IsNull } from 'typeorm'; import config from '@/config/index.js'; import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; import { verifyLogin, hash } from '../2fa.js'; -import { randomBytes } from 'node:crypto'; -import { IsNull } from 'typeorm'; +import signin from '../common/signin.js'; export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); const body = ctx.request.body as any; + + const instance = await fetchMeta(true); + const username = body['username']; const password = body['password']; const token = body['token']; @@ -79,6 +84,18 @@ export default async (ctx: Koa.Context) => { } if (!profile.twoFactorEnabled) { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + if (same) { signin(ctx, user); return; @@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => { body.credentialId .replace(/-/g, '+') .replace(/_/g, '/'), - 'base64' + 'base64', ).toString('hex'), }); diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 027d078ce1..c34e043145 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -4,11 +4,11 @@ import { dirname } from 'node:path'; import Koa from 'koa'; import send from 'koa-send'; import rename from 'rename'; -import * as tmp from 'tmp'; import { serverLogger } from '../index.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { DriveFiles } from '@/models/index.js'; import { InternalStorage } from '@/services/drive/internal-storage.js'; +import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js'; @@ -50,12 +50,7 @@ export default async function(ctx: Koa.Context) { if (!file.storedInternal) { if (file.isLink && file.uri) { // 期限切れリモートファイル - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); try { await downloadUrl(file.uri, path); diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index c1a2a6dfff..f31de2b7f4 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -2,6 +2,7 @@ * Core Server */ +import cluster from 'node:cluster'; import * as fs from 'node:fs'; import * as http from 'node:http'; import Koa from 'koa'; @@ -88,10 +89,10 @@ router.get('/avatar/@:acct', async ctx => { }); router.get('/identicon/:x', async ctx => { - const [temp] = await createTemp(); + const [temp, cleanup] = await createTemp(); await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); ctx.set('Content-Type', 'image/png'); - ctx.body = fs.createReadStream(temp); + ctx.body = fs.createReadStream(temp).on('close', () => cleanup()); }); router.get('/verify-email/:code', async ctx => { @@ -142,5 +143,26 @@ export default () => new Promise(resolve => { initializeStreamingServer(server); + server.on('error', e => { + switch ((e as any).code) { + case 'EACCES': + serverLogger.error(`You do not have permission to listen on port ${config.port}.`); + break; + case 'EADDRINUSE': + serverLogger.error(`Port ${config.port} is already in use by another process.`); + break; + default: + serverLogger.error(e); + break; + } + + if (cluster.isWorker) { + process.send!('listenFailed'); + } else { + // disableClustering + process.exit(1); + } + }); + server.listen(config.port, resolve); }); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 751e8619bf..a9ee0df4f1 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -58,15 +58,11 @@ ? `?salt=${localStorage.getItem('salt')}` : ''; - const script = document.createElement('script'); - script.setAttribute('src', `/assets/app.${v}.js${salt}`); - script.setAttribute('async', 'true'); - script.setAttribute('defer', 'true'); - script.addEventListener('error', async () => { - await checkUpdate(); - renderError('APP_FETCH_FAILED'); - }); - document.head.appendChild(script); + import(`/assets/${CLIENT_ENTRY}${salt}`) + .catch(async () => { + await checkUpdate(); + renderError('APP_FETCH_FAILED'); + }) //#endregion //#region Theme diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 061ea50609..9e31f2389e 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -4,6 +4,7 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { PathOrFileDescriptor, readFileSync } from 'node:fs'; import ms from 'ms'; import Koa from 'koa'; import Router from '@koa/router'; @@ -73,6 +74,9 @@ app.use(views(_dirname + '/views', { extension: 'pug', options: { version: config.version, + clientEntry: () => process.env.NODE_ENV === 'production' ? + config.clientEntry : + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''), config, }, })); diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 9c4cd4b9bf..d59f00fe16 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -39,28 +39,24 @@ html { width: 28px; height: 28px; transform: translateY(70px); + color: var(--accent); } - -#splashSpinner:before, -#splashSpinner:after { - content: " "; - display: block; - box-sizing: border-box; +#splashSpinner > .spinner { + position: absolute; + top: 0; + left: 0; width: 28px; height: 28px; - border-radius: 50%; - border: solid 4px; + fill-rule: evenodd; + clip-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 1.5; } - -#splashSpinner:before { - border-color: currentColor; - opacity: 0.3; +#splashSpinner > .spinner.bg { + opacity: 0.275; } - -#splashSpinner:after { - position: absolute; - top: 0; - border-color: currentColor transparent transparent transparent; +#splashSpinner > .spinner.fg { animation: splashSpinner 0.5s linear infinite; } diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 1513208310..a488e51171 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -50,6 +50,10 @@ html style include ../style.css + script. + var VERSION = "#{version}"; + var CLIENT_ENTRY = "#{clientEntry()}"; + script include ../boot.js @@ -61,4 +65,14 @@ html div#splash img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner + <svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1,0,0,1,12,12)"> + <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/> + </g> + </svg> + <svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1,0,0,1,12,12)"> + <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/> + </g> + </svg> block content diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts index 7530b4e0ba..1d094f2edd 100644 --- a/packages/backend/src/server/well-known.ts +++ b/packages/backend/src/server/well-known.ts @@ -41,6 +41,7 @@ router.options(allPath, async ctx => { router.get('/.well-known/host-meta', async ctx => { ctx.set('Content-Type', xrd); ctx.body = XRD({ element: 'Link', attributes: { + rel: 'lrdd', type: xrd, template: `${config.url}${webFingerPath}?resource={uri}`, } }); diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index da93bc97c7..ca12ab8d3d 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -1,38 +1,31 @@ import * as fs from 'node:fs'; -import * as tmp from 'tmp'; +import * as path from 'node:path'; +import { createTemp } from '@/misc/create-temp.js'; import { IImage, convertToJpeg } from './image-processor.js'; -import * as FFmpeg from 'fluent-ffmpeg'; +import FFmpeg from 'fluent-ffmpeg'; -export async function GenerateVideoThumbnail(path: string): Promise<IImage> { - const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); +export async function GenerateVideoThumbnail(source: string): Promise<IImage> { + const [file, cleanup] = await createTemp(); + const parsed = path.parse(file); - await new Promise((res, rej) => { - FFmpeg({ - source: path, - }) - .on('end', res) - .on('error', rej) - .screenshot({ - folder: outDir, - filename: 'output.png', - count: 1, - timestamps: ['5%'], + try { + await new Promise((res, rej) => { + FFmpeg({ + source, + }) + .on('end', res) + .on('error', rej) + .screenshot({ + folder: parsed.dir, + filename: parsed.base, + count: 1, + timestamps: ['5%'], + }); }); - }); - - const outPath = `${outDir}/output.png`; - - // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - const thumbnail = await convertToJpeg(outPath, 498, 280); - - // cleanup - await fs.promises.unlink(outPath); - cleanup(); - return thumbnail; + // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) + return await convertToJpeg(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 79b1b8c2e1..001fc49ee4 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -45,29 +45,20 @@ export async function uploadFromUrl({ // Create temp file const [path, cleanup] = await createTemp(); - // write content at URL to temp file - await downloadUrl(url, path); - - let driveFile: DriveFile; - let error; - try { - driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); + // 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 }); logger.succ(`Got: ${driveFile.id}`); + return driveFile!; } catch (e) { - error = e; logger.error(`Failed to create drive file: ${e}`, { url: url, e: e, }); - } - - // clean-up - cleanup(); - - if (error) { - throw error; - } else { - return driveFile!; + throw e; + } finally { + cleanup(); } } diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts index d5294c5fe8..029c388dc2 100644 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ b/packages/backend/src/services/fetch-instance-metadata.ts @@ -1,5 +1,6 @@ import { DOMWindow, JSDOM } from 'jsdom'; import fetch from 'node-fetch'; +import tinycolor from 'tinycolor2'; import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; import { Instance } from '@/models/entities/instance.js'; import { Instances } from '@/models/index.js'; @@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul } async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { - if (doc) { - const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content'); + const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color; - if (themeColor) { - return themeColor; - } - } - - if (manifest) { - return manifest.theme_color; + if (themeColor) { + const color = new tinycolor(themeColor); + if (color.isValid()) return color.toHexString(); } return null; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index f14bc2059b..ceb5e8cc71 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -187,6 +187,8 @@ export default async (user: { id: User['id']; username: User['username']; host: if (data.text) { data.text = data.text.trim(); + } else { + data.text = null; } let tags = data.apHashtags; diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index 5a0948bca9..83d302826a 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note, } } + // check visibility + if (!await Notes.isVisibleForMe(note, user.id)) { + throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); + } + // TODO: cache reaction = await toDbReaction(reaction, user.host); |