Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
		
						commit
						465531d56c
					
				
					 266 changed files with 7852 additions and 5442 deletions
				
			
		|  | @ -16,6 +16,17 @@ module.exports = { | |||
| 					'position': 'after' | ||||
| 				} | ||||
| 			], | ||||
| 		}] | ||||
| 		}], | ||||
| 		'no-restricted-globals': [ | ||||
| 			'error', | ||||
| 			{ | ||||
| 				'name': '__dirname', | ||||
| 				'message': 'Not in ESModule. Use `import.meta.url` instead.' | ||||
| 			}, | ||||
| 			{ | ||||
| 				'name': '__filename', | ||||
| 				'message': 'Not in ESModule. Use `import.meta.url` instead.' | ||||
| 			} | ||||
| 	] | ||||
| 	}, | ||||
| }; | ||||
|  |  | |||
|  | @ -5,6 +5,6 @@ | |||
| 		"loader=./test/loader.js" | ||||
| 	], | ||||
| 	"slow": 1000, | ||||
| 	"timeout": 35000, | ||||
| 	"timeout": 3000, | ||||
| 	"exit": true | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,36 @@ | |||
| import tinycolor from 'tinycolor2'; | ||||
| 
 | ||||
| export class uniformThemecolor1652859567549 { | ||||
| 	name = 'uniformThemecolor1652859567549' | ||||
| 
 | ||||
| 	async up(queryRunner) { | ||||
| 		const formatColor = (color) => { | ||||
| 			let tc = new tinycolor(color); | ||||
| 			if (tc.isValid()) { | ||||
| 				return tc.toHexString(); | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
| 		await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL') | ||||
| 		.then(instances => Promise.all(instances.map(instance => { | ||||
| 			// update theme color to uniform format, e.g. #00ff00
 | ||||
| 			// invalid theme colors get set to null
 | ||||
| 			return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]); | ||||
| 		}))); | ||||
| 
 | ||||
| 		// also fix own theme color
 | ||||
| 		await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1') | ||||
| 		.then(metas => { | ||||
| 			if (metas.length > 0) { | ||||
| 				return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async down(queryRunner) { | ||||
| 		// The original representation is not stored, so migrating back is not possible.
 | ||||
| 		// The new format also works in older versions so this is not a problem.
 | ||||
| 	} | ||||
| } | ||||
|  | @ -6,7 +6,7 @@ | |||
| 		"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", | ||||
| 		"watch": "node watch.mjs", | ||||
| 		"lint": "eslint --quiet \"src/**/*.ts\"", | ||||
| 		"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", | ||||
| 		"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", | ||||
| 		"test": "npm run mocha" | ||||
| 	}, | ||||
| 	"resolutions": { | ||||
|  | @ -15,25 +15,24 @@ | |||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@bull-board/koa": "3.10.4", | ||||
| 		"@discordapp/twemoji": "13.1.1", | ||||
| 		"@discordapp/twemoji": "14.0.2", | ||||
| 		"@elastic/elasticsearch": "7.11.0", | ||||
| 		"@koa/cors": "3.1.0", | ||||
| 		"@koa/multer": "3.0.0", | ||||
| 		"@koa/router": "9.0.1", | ||||
| 		"@sinonjs/fake-timers": "9.1.1", | ||||
| 		"@peertube/http-signature": "1.6.0", | ||||
| 		"@sinonjs/fake-timers": "9.1.2", | ||||
| 		"@syuilo/aiscript": "0.11.1", | ||||
| 		"@typescript-eslint/eslint-plugin": "5.20.0", | ||||
| 		"@typescript-eslint/parser": "5.20.0", | ||||
| 		"abort-controller": "3.0.0", | ||||
| 		"ajv": "8.11.0", | ||||
| 		"archiver": "5.3.1", | ||||
| 		"autobind-decorator": "2.4.0", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"aws-sdk": "2.1120.0", | ||||
| 		"aws-sdk": "2.1135.0", | ||||
| 		"bcryptjs": "2.4.3", | ||||
| 		"blurhash": "1.1.5", | ||||
| 		"broadcast-channel": "4.11.0", | ||||
| 		"bull": "4.8.2", | ||||
| 		"broadcast-channel": "4.12.0", | ||||
| 		"bull": "4.8.3", | ||||
| 		"cacheable-lookup": "6.0.4", | ||||
| 		"cbor": "8.1.0", | ||||
| 		"chalk": "5.0.1", | ||||
|  | @ -44,22 +43,19 @@ | |||
| 		"date-fns": "2.28.0", | ||||
| 		"deep-email-validator": "0.1.21", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "8.14.0", | ||||
| 		"eslint-plugin-import": "2.26.0", | ||||
| 		"feed": "4.2.2", | ||||
| 		"file-type": "17.1.1", | ||||
| 		"fluent-ffmpeg": "2.1.2", | ||||
| 		"got": "12.0.3", | ||||
| 		"got": "12.0.4", | ||||
| 		"hpagent": "0.1.2", | ||||
| 		"http-signature": "1.3.6", | ||||
| 		"ip-cidr": "3.0.7", | ||||
| 		"ip-cidr": "3.0.8", | ||||
| 		"is-svg": "4.3.2", | ||||
| 		"js-yaml": "4.1.0", | ||||
| 		"jsdom": "19.0.0", | ||||
| 		"json5": "2.2.1", | ||||
| 		"json5-loader": "4.0.1", | ||||
| 		"jsonld": "5.2.0", | ||||
| 		"jsrsasign": "10.5.19", | ||||
| 		"jsrsasign": "10.5.22", | ||||
| 		"koa": "2.13.4", | ||||
| 		"koa-bodyparser": "4.3.0", | ||||
| 		"koa-favicon": "2.1.0", | ||||
|  | @ -69,19 +65,18 @@ | |||
| 		"koa-send": "5.0.1", | ||||
| 		"koa-slow": "2.1.0", | ||||
| 		"koa-views": "7.0.2", | ||||
| 		"mfm-js": "0.21.0", | ||||
| 		"mfm-js": "0.22.1", | ||||
| 		"mime-types": "2.1.35", | ||||
| 		"misskey-js": "0.0.14", | ||||
| 		"mocha": "9.2.2", | ||||
| 		"mocha": "10.0.0", | ||||
| 		"ms": "3.0.0-canary.1", | ||||
| 		"multer": "1.4.4", | ||||
| 		"nested-property": "4.0.0", | ||||
| 		"node-fetch": "3.2.3", | ||||
| 		"nodemailer": "6.7.3", | ||||
| 		"node-fetch": "3.2.4", | ||||
| 		"nodemailer": "6.7.5", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"parse5": "6.0.1", | ||||
| 		"pg": "8.7.3", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"private-ip": "2.3.3", | ||||
| 		"probe-image-size": "7.2.3", | ||||
| 		"promise-limit": "2.7.0", | ||||
|  | @ -101,33 +96,32 @@ | |||
| 		"s-age": "1.1.2", | ||||
| 		"sanitize-html": "2.7.0", | ||||
| 		"semver": "7.3.7", | ||||
| 		"sharp": "0.30.4", | ||||
| 		"sharp": "0.29.3", | ||||
| 		"speakeasy": "2.0.0", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"style-loader": "3.3.1", | ||||
| 		"summaly": "2.5.0", | ||||
| 		"syslog-pro": "1.0.0", | ||||
| 		"systeminformation": "5.11.14", | ||||
| 		"systeminformation": "5.11.15", | ||||
| 		"tinycolor2": "1.4.2", | ||||
| 		"tmp": "0.2.1", | ||||
| 		"ts-loader": "9.2.8", | ||||
| 		"ts-node": "10.7.0", | ||||
| 		"tsc-alias": "1.4.1", | ||||
| 		"tsconfig-paths": "3.14.1", | ||||
| 		"ts-loader": "9.3.0", | ||||
| 		"ts-node": "10.8.0", | ||||
| 		"tsc-alias": "1.6.7", | ||||
| 		"tsconfig-paths": "4.0.0", | ||||
| 		"twemoji-parser": "14.0.0", | ||||
| 		"typeorm": "0.3.6", | ||||
| 		"typescript": "4.6.3", | ||||
| 		"ulid": "2.3.0", | ||||
| 		"unzipper": "0.10.11", | ||||
| 		"uuid": "8.3.2", | ||||
| 		"web-push": "3.4.5", | ||||
| 		"web-push": "3.5.0", | ||||
| 		"websocket": "1.0.34", | ||||
| 		"ws": "8.5.0", | ||||
| 		"ws": "8.6.0", | ||||
| 		"xev": "3.0.2" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@redocly/openapi-core": "1.0.0-beta.93", | ||||
| 		"@redocly/openapi-core": "1.0.0-beta.97", | ||||
| 		"@types/semver": "7.3.9", | ||||
| 		"@types/bcryptjs": "2.4.2", | ||||
| 		"@types/bull": "3.15.8", | ||||
|  | @ -138,7 +132,7 @@ | |||
| 		"@types/js-yaml": "4.0.5", | ||||
| 		"@types/jsdom": "16.2.14", | ||||
| 		"@types/jsonld": "1.5.6", | ||||
| 		"@types/jsrsasign": "10.2.1", | ||||
| 		"@types/jsrsasign": "10.5.1", | ||||
| 		"@types/koa": "2.13.4", | ||||
| 		"@types/koa-bodyparser": "4.3.7", | ||||
| 		"@types/koa-cors": "0.0.2", | ||||
|  | @ -151,12 +145,11 @@ | |||
| 		"@types/koa__multer": "2.0.4", | ||||
| 		"@types/koa__router": "8.0.11", | ||||
| 		"@types/mocha": "9.1.1", | ||||
| 		"@types/node": "17.0.25", | ||||
| 		"@types/node": "17.0.35", | ||||
| 		"@types/node-fetch": "3.0.3", | ||||
| 		"@types/nodemailer": "6.4.4", | ||||
| 		"@types/oauth": "0.9.1", | ||||
| 		"@types/parse5": "6.0.3", | ||||
| 		"@types/portscanner": "2.1.1", | ||||
| 		"@types/pug": "2.0.6", | ||||
| 		"@types/punycode": "2.1.0", | ||||
| 		"@types/qrcode": "1.4.2", | ||||
|  | @ -174,6 +167,12 @@ | |||
| 		"@types/web-push": "3.3.2", | ||||
| 		"@types/websocket": "1.0.5", | ||||
| 		"@types/ws": "8.5.3", | ||||
| 		"@typescript-eslint/eslint-plugin": "5.26.0", | ||||
| 		"@typescript-eslint/parser": "5.26.0", | ||||
| 		"typescript": "4.7.2", | ||||
| 		"eslint": "8.16.0", | ||||
| 		"eslint-plugin-import": "2.26.0", | ||||
| 
 | ||||
| 		"cross-env": "7.0.3", | ||||
| 		"execa": "6.1.0" | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| declare module 'http-signature' { | ||||
| declare module '@peertube/http-signature' { | ||||
| 	import { IncomingMessage, ClientRequest } from 'node:http'; | ||||
| 
 | ||||
| 	interface ISignature { | ||||
|  |  | |||
|  | @ -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(); | ||||
| 		}); | ||||
|  |  | |||
|  | @ -46,7 +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_\//, ''); | ||||
| 	mixin.clientEntry = clientManifest['src/init.ts']; | ||||
| 
 | ||||
| 	if (!config.redis.prefix) config.redis.prefix = mixin.host; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -48,6 +48,7 @@ export class Cache<T> { | |||
| 
 | ||||
| 		// Cache MISS
 | ||||
| 		const value = await fetcher(); | ||||
| 		this.set(key, value); | ||||
| 		return value; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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]); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({ | |||
| 				return true; | ||||
| 			} else { | ||||
| 				// 指定されているかどうか
 | ||||
| 				const specified = note.visibleUserIds.some((id: any) => meId === id); | ||||
| 
 | ||||
| 				if (specified) { | ||||
| 					return true; | ||||
| 				} else { | ||||
| 					return false; | ||||
| 				} | ||||
| 				return note.visibleUserIds.some((id: any) => meId === id); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -168,16 +162,25 @@ 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.count({ | ||||
| 						where: { | ||||
| 							followeeId: note.userId, | ||||
| 							followerId: meId, | ||||
| 						}, | ||||
| 						take: 1, | ||||
| 					}), | ||||
| 					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 > 0 || (note.userHost != null && user.host != null); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({ | |||
| 	//#endregion
 | ||||
| 
 | ||||
| 	async getRelation(me: User['id'], target: User['id']) { | ||||
| 		const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ | ||||
| 			Followings.findOneBy({ | ||||
| 				followerId: me, | ||||
| 				followeeId: target, | ||||
| 			}), | ||||
| 			Followings.findOneBy({ | ||||
| 				followerId: target, | ||||
| 				followeeId: me, | ||||
| 			}), | ||||
| 			FollowRequests.findOneBy({ | ||||
| 				followerId: me, | ||||
| 				followeeId: target, | ||||
| 			}), | ||||
| 			FollowRequests.findOneBy({ | ||||
| 				followerId: target, | ||||
| 				followeeId: me, | ||||
| 			}), | ||||
| 			Blockings.findOneBy({ | ||||
| 				blockerId: me, | ||||
| 				blockeeId: target, | ||||
| 			}), | ||||
| 			Blockings.findOneBy({ | ||||
| 				blockerId: target, | ||||
| 				blockeeId: me, | ||||
| 			}), | ||||
| 			Mutings.findOneBy({ | ||||
| 				muterId: me, | ||||
| 				muteeId: target, | ||||
| 			}), | ||||
| 		]); | ||||
| 
 | ||||
| 		return { | ||||
| 		return awaitAll({ | ||||
| 			id: target, | ||||
| 			isFollowing: following1 != null, | ||||
| 			hasPendingFollowRequestFromYou: followReq1 != null, | ||||
| 			hasPendingFollowRequestToYou: followReq2 != null, | ||||
| 			isFollowed: following2 != null, | ||||
| 			isBlocking: toBlocking != null, | ||||
| 			isBlocked: fromBlocked != null, | ||||
| 			isMuted: mute != null, | ||||
| 		}; | ||||
| 			isFollowing: Followings.count({ | ||||
| 				where: { | ||||
| 					followerId: me, | ||||
| 					followeeId: target, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			isFollowed: Followings.count({ | ||||
| 				where: { | ||||
| 					followerId: target, | ||||
| 					followeeId: me, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			hasPendingFollowRequestFromYou: FollowRequests.count({ | ||||
| 				where: { | ||||
| 					followerId: me, | ||||
| 					followeeId: target, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			hasPendingFollowRequestToYou: FollowRequests.count({ | ||||
| 				where: { | ||||
| 					followerId: target, | ||||
| 					followeeId: me, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			isBlocking: Blockings.count({ | ||||
| 				where: { | ||||
| 					blockerId: me, | ||||
| 					blockeeId: target, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			isBlocked: Blockings.count({ | ||||
| 				where: { | ||||
| 					blockerId: target, | ||||
| 					blockeeId: me, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 			isMuted: Mutings.count({ | ||||
| 				where: { | ||||
| 					muterId: me, | ||||
| 					muteeId: target, | ||||
| 				}, | ||||
| 				take: 1, | ||||
| 			}).then(n => n > 0), | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> { | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = blockings[blockings.length - 1].id; | ||||
| 
 | ||||
| 		for (const block of blockings) { | ||||
| 			const u = await Users.findOneBy({ id: block.blockeeId }); | ||||
| 			if (u == null) { | ||||
| 				exportedCount++; continue; | ||||
| 			if (blockings.length === 0) { | ||||
| 				job.progress(100); | ||||
| 				break; | ||||
| 			} | ||||
| 
 | ||||
| 			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(); | ||||
| 					} | ||||
| 			cursor = blockings[blockings.length - 1].id; | ||||
| 
 | ||||
| 			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(); | ||||
| 						} | ||||
| 					}); | ||||
| 				}); | ||||
| 				exportedCount++; | ||||
| 			} | ||||
| 
 | ||||
| 			const total = await Blockings.countBy({ | ||||
| 				blockerId: user.id, | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 
 | ||||
| 			job.progress(exportedCount / total); | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Blockings.countBy({ | ||||
| 			blockerId: user.id, | ||||
| 		}); | ||||
| 		stream.end(); | ||||
| 		logger.succ(`Exported to: ${path}`); | ||||
| 
 | ||||
| 		job.progress(exportedCount / total); | ||||
| 		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(); | ||||
| 	} | ||||
| 
 | ||||
| 	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 }); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  |  | |||
|  | @ -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 { createTemp, 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 }, | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = followings[followings.length - 1].id; | ||||
| 
 | ||||
| 		for (const following of followings) { | ||||
| 			const u = await Users.findOneBy({ id: following.followeeId }); | ||||
| 			if (u == null) { | ||||
| 				continue; | ||||
| 			if (followings.length === 0) { | ||||
| 				break; | ||||
| 			} | ||||
| 
 | ||||
| 			if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			cursor = followings[followings.length - 1].id; | ||||
| 
 | ||||
| 			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(); | ||||
| 					} | ||||
| 			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; | ||||
| 				} | ||||
| 
 | ||||
| 				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}`); | ||||
| 
 | ||||
| 		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(); | ||||
| 	} | ||||
| 
 | ||||
| 	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 }); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = mutes[mutes.length - 1].id; | ||||
| 
 | ||||
| 		for (const mute of mutes) { | ||||
| 			const u = await Users.findOneBy({ id: mute.muteeId }); | ||||
| 			if (u == null) { | ||||
| 				exportedCount++; continue; | ||||
| 			if (mutes.length === 0) { | ||||
| 				job.progress(100); | ||||
| 				break; | ||||
| 			} | ||||
| 
 | ||||
| 			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(); | ||||
| 					} | ||||
| 			cursor = mutes[mutes.length - 1].id; | ||||
| 
 | ||||
| 			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(); | ||||
| 						} | ||||
| 					}); | ||||
| 				}); | ||||
| 				exportedCount++; | ||||
| 			} | ||||
| 
 | ||||
| 			const total = await Mutings.countBy({ | ||||
| 				muterId: user.id, | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 
 | ||||
| 			job.progress(exportedCount / total); | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Mutings.countBy({ | ||||
| 			muterId: user.id, | ||||
| 		}); | ||||
| 		stream.end(); | ||||
| 		logger.succ(`Exported to: ${path}`); | ||||
| 
 | ||||
| 		job.progress(exportedCount / total); | ||||
| 		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(); | ||||
| 	} | ||||
| 
 | ||||
| 	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 }); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 
 | ||||
| 		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 }); | ||||
| 			if (notes.length === 0) { | ||||
| 				job.progress(100); | ||||
| 				break; | ||||
| 			} | ||||
| 			const content = JSON.stringify(serialize(note, poll)); | ||||
| 			const isFirst = exportedNotesCount === 0; | ||||
| 			await write(isFirst ? content : ',\n' + content); | ||||
| 			exportedNotesCount++; | ||||
| 
 | ||||
| 			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 }); | ||||
| 				} | ||||
| 				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, | ||||
| 			}); | ||||
| 
 | ||||
| 			job.progress(exportedNotesCount / total); | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Notes.countBy({ | ||||
| 			userId: user.id, | ||||
| 		}); | ||||
| 		await write(']'); | ||||
| 
 | ||||
| 		job.progress(exportedNotesCount / total); | ||||
| 		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 }); | ||||
| 
 | ||||
| 		logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	} finally { | ||||
| 		cleanup(); | ||||
| 	} | ||||
| 
 | ||||
| 	await write(']'); | ||||
| 
 | ||||
| 	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 }); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 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 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(); | ||||
| 						} | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		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 }); | ||||
| 
 | ||||
| 		logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	} finally { | ||||
| 		cleanup(); | ||||
| 	} | ||||
| 
 | ||||
| 	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 }); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile.id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  |  | |||
|  | @ -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}`); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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 */ | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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}`; | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ import promiseLimit from 'promise-limit'; | |||
| import config from '@/config/index.js'; | ||||
| import Resolver from '../resolver.js'; | ||||
| import post from '@/services/note/create.js'; | ||||
| import { resolvePerson, updatePerson } from './person.js'; | ||||
| import { resolvePerson } from './person.js'; | ||||
| import { resolveImage } from './image.js'; | ||||
| import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; | ||||
| import { CacheableRemoteUser } from '@/models/entities/user.js'; | ||||
| import { htmlToMfm } from '../misc/html-to-mfm.js'; | ||||
| import { extractApHashtags } from './tag.js'; | ||||
| import { unique, toArray, toSingle } from '@/prelude/array.js'; | ||||
|  | @ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; | |||
| import { DriveFile } from '@/models/entities/drive-file.js'; | ||||
| import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; | ||||
| import { extractDbHost, toPuny } from '@/misc/convert-host.js'; | ||||
| import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; | ||||
| import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; | ||||
| import { Note } from '@/models/entities/note.js'; | ||||
| import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; | ||||
| import { Emoji } from '@/models/entities/emoji.js'; | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js'; | |||
| export const renderActivity = (x: any): IActivity | null => { | ||||
| 	if (x == null) return null; | ||||
| 
 | ||||
| 	if (x !== null && typeof x === 'object' && x.id == null) { | ||||
| 	if (typeof x === 'object' && x.id == null) { | ||||
| 		x.id = `${config.url}/${uuid()}`; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -2,10 +2,11 @@ import Koa from 'koa'; | |||
| import { performance } from 'perf_hooks'; | ||||
| import { limiter } from './limiter.js'; | ||||
| import { CacheableLocalUser, User } from '@/models/entities/user.js'; | ||||
| import endpoints, { IEndpoint } from './endpoints.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 IPCIDR from 'ip-cidr'; | ||||
| 
 | ||||
| const accessDenied = { | ||||
| 	message: 'Access denied.', | ||||
|  | @ -15,6 +16,7 @@ const accessDenied = { | |||
| 
 | ||||
| export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { | ||||
| 	const isSecure = user != null && token == null; | ||||
| 	const isModerator = user != null && (user.isModerator || user.isAdmin); | ||||
| 
 | ||||
| 	const ep = endpoints.find(e => e.name === endpoint); | ||||
| 
 | ||||
|  | @ -31,6 +33,37 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi | |||
| 		throw new ApiError(accessDenied); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.requireCredential && ep.meta.limit && !isModerator) { | ||||
| 		// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
 | ||||
| 		let limitActor: string; | ||||
| 		if (user) { | ||||
| 			limitActor = user.id; | ||||
| 		} else { | ||||
| 			// because a single person may control many IPv6 addresses,
 | ||||
| 			// only a /64 subnet prefix of any IP will be taken into account.
 | ||||
| 			// (this means for IPv4 the entire address is used)
 | ||||
| 			const ip = IPCIDR.createAddress(ctx.ip).mask(64); | ||||
| 
 | ||||
| 			limitActor = 'ip-' + parseInt(ip, 2).toString(36); | ||||
| 		} | ||||
| 
 | ||||
| 		const limit = Object.assign({}, ep.meta.limit); | ||||
| 
 | ||||
| 		if (!limit.key) { | ||||
| 			limit.key = ep.name; | ||||
| 		} | ||||
| 
 | ||||
| 		// Rate limit
 | ||||
| 		await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => { | ||||
| 			throw new ApiError({ | ||||
| 				message: 'Rate limit exceeded. Please try again later.', | ||||
| 				code: 'RATE_LIMIT_EXCEEDED', | ||||
| 				id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', | ||||
| 				httpStatusCode: 429, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.requireCredential && user == null) { | ||||
| 		throw new ApiError({ | ||||
| 			message: 'Credential required.', | ||||
|  | @ -53,7 +86,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi | |||
| 		throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { | ||||
| 	if (ep.meta.requireModerator && !isModerator) { | ||||
| 		throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -65,18 +98,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) { | ||||
| 		// Rate limit
 | ||||
| 		await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => { | ||||
| 			throw new ApiError({ | ||||
| 				message: 'Rate limit exceeded. Please try again later.', | ||||
| 				code: 'RATE_LIMIT_EXCEEDED', | ||||
| 				id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', | ||||
| 				httpStatusCode: 429, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	// Cast non JSON input
 | ||||
| 	if (ep.meta.requireFile && ep.params.properties) { | ||||
| 		for (const k of Object.keys(ep.params.properties)) { | ||||
|  |  | |||
|  | @ -654,7 +654,6 @@ export interface IEndpointMeta { | |||
| 	/** | ||||
| 	 * エンドポイントのリミテーションに関するやつ | ||||
| 	 * 省略した場合はリミテーションは無いものとして解釈されます。 | ||||
| 	 * また、withCredential が false の場合はリミテーションを行うことはできません。 | ||||
| 	 */ | ||||
| 	readonly limit?: { | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 }, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -134,7 +134,7 @@ export const paramDef = { | |||
| 		{ | ||||
| 			// (re)note with text, files and poll are optional
 | ||||
| 			properties: { | ||||
| 				text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, | ||||
| 				text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, | ||||
| 			}, | ||||
| 			required: ['text'], | ||||
| 		}, | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 }); | ||||
|  |  | |||
|  | @ -1,25 +1,17 @@ | |||
| import Limiter from 'ratelimiter'; | ||||
| import { redisClient } from '../../db/redis.js'; | ||||
| import { IEndpoint } from './endpoints.js'; | ||||
| import * as Acct from '@/misc/acct.js'; | ||||
| import { IEndpointMeta } from './endpoints.js'; | ||||
| import { CacheableLocalUser, User } from '@/models/entities/user.js'; | ||||
| import Logger from '@/services/logger.js'; | ||||
| 
 | ||||
| const logger = new Logger('limiter'); | ||||
| 
 | ||||
| export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => { | ||||
| 	const limitation = endpoint.meta.limit; | ||||
| 
 | ||||
| 	const key = Object.prototype.hasOwnProperty.call(limitation, 'key') | ||||
| 		? limitation.key | ||||
| 		: endpoint.name; | ||||
| 
 | ||||
| 	const hasShortTermLimit = | ||||
| 		Object.prototype.hasOwnProperty.call(limitation, 'minInterval'); | ||||
| export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => { | ||||
| 	const hasShortTermLimit = typeof limitation.minInterval === 'number'; | ||||
| 
 | ||||
| 	const hasLongTermLimit = | ||||
| 		Object.prototype.hasOwnProperty.call(limitation, 'duration') && | ||||
| 		Object.prototype.hasOwnProperty.call(limitation, 'max'); | ||||
| 		typeof limitation.duration === 'number' && | ||||
| 		typeof limitation.max === 'number'; | ||||
| 
 | ||||
| 	if (hasShortTermLimit) { | ||||
| 		min(); | ||||
|  | @ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp | |||
| 	// Short-term limit
 | ||||
| 	function min(): void { | ||||
| 		const minIntervalLimiter = new Limiter({ | ||||
| 			id: `${user.id}:${key}:min`, | ||||
| 			id: `${actor}:${limitation.key}:min`, | ||||
| 			duration: limitation.minInterval, | ||||
| 			max: 1, | ||||
| 			db: redisClient, | ||||
|  | @ -43,7 +35,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp | |||
| 				return reject('ERR'); | ||||
| 			} | ||||
| 
 | ||||
| 			logger.debug(`@${Acct.toString(user)} ${endpoint.name} min remaining: ${info.remaining}`); | ||||
| 			logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); | ||||
| 
 | ||||
| 			if (info.remaining === 0) { | ||||
| 				reject('BRIEF_REQUEST_INTERVAL'); | ||||
|  | @ -60,7 +52,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp | |||
| 	// Long term limit
 | ||||
| 	function max(): void { | ||||
| 		const limiter = new Limiter({ | ||||
| 			id: `${user.id}:${key}`, | ||||
| 			id: `${actor}:${limitation.key}`, | ||||
| 			duration: limitation.duration, | ||||
| 			max: limitation.max, | ||||
| 			db: redisClient, | ||||
|  | @ -71,7 +63,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp | |||
| 				return reject('ERR'); | ||||
| 			} | ||||
| 
 | ||||
| 			logger.debug(`@${Acct.toString(user)} ${endpoint.name} max remaining: ${info.remaining}`); | ||||
| 			logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); | ||||
| 
 | ||||
| 			if (info.remaining === 0) { | ||||
| 				reject('RATE_LIMIT_EXCEEDED'); | ||||
|  |  | |||
|  | @ -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, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { genId } from '@/misc/gen-id.js'; | |||
| import { verifyLogin, hash } from '../2fa.js'; | ||||
| import { randomBytes } from 'node:crypto'; | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { limiter } from '../limiter.js'; | ||||
| 
 | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	ctx.set('Access-Control-Allow-Origin', config.url); | ||||
|  | @ -24,6 +25,21 @@ export default async (ctx: Koa.Context) => { | |||
| 		ctx.body = { error }; | ||||
| 	} | ||||
| 
 | ||||
| 	try { | ||||
| 		// not more than 1 attempt per second and not more than 10 attempts per hour
 | ||||
| 		await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, ctx.ip); | ||||
| 	} catch (err) { | ||||
| 		ctx.status = 429; | ||||
| 		ctx.body = { | ||||
| 			error: { | ||||
| 				message: 'Too many failed attempts to sign in. Try again later.', | ||||
| 				code: 'TOO_MANY_AUTHENTICATION_FAILURES', | ||||
| 				id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', | ||||
| 			}, | ||||
| 		}; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	if (typeof username !== 'string') { | ||||
| 		ctx.status = 400; | ||||
| 		return; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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); | ||||
| }); | ||||
|  |  | |||
|  | @ -54,14 +54,10 @@ | |||
| 	//#endregion
 | ||||
| 
 | ||||
| 	//#region Script
 | ||||
| 	const salt = localStorage.getItem('salt') | ||||
| 		? `?salt=${localStorage.getItem('salt')}` | ||||
| 		: ''; | ||||
| 
 | ||||
| 	import(`/assets/${CLIENT_ENTRY}${salt}`) | ||||
| 		.catch(async () => { | ||||
| 	import(`/assets/${CLIENT_ENTRY}`) | ||||
| 		.catch(async e => { | ||||
| 			await checkUpdate(); | ||||
| 			renderError('APP_FETCH_FAILED'); | ||||
| 			renderError('APP_FETCH_FAILED', JSON.stringify(e)); | ||||
| 		}) | ||||
| 	//#endregion
 | ||||
| 
 | ||||
|  | @ -142,9 +138,6 @@ | |||
| 
 | ||||
| 	// eslint-disable-next-line no-inner-declarations
 | ||||
| 	function refresh() { | ||||
| 		// Random
 | ||||
| 		localStorage.setItem('salt', Math.random().toString().substr(2, 8)); | ||||
| 
 | ||||
| 		// Clear cache (service worker)
 | ||||
| 		try { | ||||
| 			navigator.serviceWorker.controller.postMessage('clear'); | ||||
|  |  | |||
|  | @ -74,9 +74,9 @@ app.use(views(_dirname + '/views', { | |||
| 	extension: 'pug', | ||||
| 	options: { | ||||
| 		version: config.version, | ||||
| 		clientEntry: () => process.env.NODE_ENV === 'production' ? | ||||
| 		getClientEntry: () => 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_\//, ''), | ||||
| 			JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], | ||||
| 		config, | ||||
| 	}, | ||||
| })); | ||||
|  | @ -247,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { | |||
| 			icon: meta.iconUrl, | ||||
| 			themeColor: meta.themeColor, | ||||
| 		}); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=30'); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 	} else { | ||||
| 		// リモートユーザーなので
 | ||||
| 		// モデレータがAPI経由で参照可能にするために404にはしない
 | ||||
|  | @ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => { | |||
| 			themeColor: meta.themeColor, | ||||
| 		}); | ||||
| 
 | ||||
| 		ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 
 | ||||
| 		return; | ||||
| 	} | ||||
|  | @ -329,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { | |||
| 		}); | ||||
| 
 | ||||
| 		if (['public'].includes(page.visibility)) { | ||||
| 			ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 			ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 		} else { | ||||
| 			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); | ||||
| 		} | ||||
|  | @ -360,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => { | |||
| 			themeColor: meta.themeColor, | ||||
| 		}); | ||||
| 
 | ||||
| 		ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 
 | ||||
| 		return; | ||||
| 	} | ||||
|  | @ -385,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => { | |||
| 			themeColor: meta.themeColor, | ||||
| 		}); | ||||
| 
 | ||||
| 		ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 
 | ||||
| 		return; | ||||
| 	} | ||||
|  | @ -409,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => { | |||
| 			themeColor: meta.themeColor, | ||||
| 		}); | ||||
| 
 | ||||
| 		ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 		ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| 
 | ||||
| 		return; | ||||
| 	} | ||||
|  | @ -468,7 +468,7 @@ router.get('(.*)', async ctx => { | |||
| 		icon: meta.iconUrl, | ||||
| 		themeColor: meta.themeColor, | ||||
| 	}); | ||||
| 	ctx.set('Cache-Control', 'public, max-age=300'); | ||||
| 	ctx.set('Cache-Control', 'public, max-age=15'); | ||||
| }); | ||||
| 
 | ||||
| // Register router
 | ||||
|  |  | |||
|  | @ -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; | ||||
| 	width: 28px; | ||||
| 	height: 28px; | ||||
| 	border-radius: 50%; | ||||
| 	border: solid 4px; | ||||
| } | ||||
| 
 | ||||
| #splashSpinner:before { | ||||
| 	border-color: currentColor; | ||||
| 	opacity: 0.3; | ||||
| } | ||||
| 
 | ||||
| #splashSpinner:after { | ||||
| #splashSpinner > .spinner { | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	border-color: currentColor transparent transparent transparent; | ||||
| 	left: 0; | ||||
| 	width: 28px; | ||||
| 	height: 28px; | ||||
| 	fill-rule: evenodd; | ||||
| 	clip-rule: evenodd; | ||||
| 	stroke-linecap: round; | ||||
| 	stroke-linejoin: round; | ||||
| 	stroke-miterlimit: 1.5; | ||||
| } | ||||
| #splashSpinner > .spinner.bg { | ||||
| 	opacity: 0.275; | ||||
| } | ||||
| #splashSpinner > .spinner.fg { | ||||
| 	animation: splashSpinner 0.5s linear infinite; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,17 +1,23 @@ | |||
| block vars | ||||
| 
 | ||||
| block loadClientEntry | ||||
| 	- const clientEntry = getClientEntry(); | ||||
| 
 | ||||
| doctype html | ||||
| 
 | ||||
| != '<!--\n' | ||||
| != '  _____ _         _           \n' | ||||
| != ' |     |_|___ ___| |_ ___ _ _ \n' | ||||
| != ' | | | | |_ -|_ -| \'_| -_| | |\n' | ||||
| != ' |_|_|_|_|___|___|_,_|___|_  |\n' | ||||
| != '                         |___|\n' | ||||
| != ' Thank you for using Misskey!\n' | ||||
| != ' If you are reading this message... how about joining the development?\n' | ||||
| != ' https://github.com/misskey-dev/misskey' | ||||
| != '\n-->\n' | ||||
| // | ||||
| 	- | ||||
| 
 | ||||
| 	  _____ _         _            | ||||
| 	 |     |_|___ ___| |_ ___ _ _  | ||||
| 	 | | | | |_ -|_ -| \'_| -_| | | | ||||
| 	 |_|_|_|_|___|___|_,_|___|_  | | ||||
| 	                         |___| | ||||
| 	 Thank you for using Misskey! | ||||
| 	 If you are reading this message... how about joining the development? | ||||
| 	 https://github.com/misskey-dev/misskey | ||||
| 	  | ||||
| 	  | ||||
| 
 | ||||
| html | ||||
| 
 | ||||
|  | @ -30,8 +36,14 @@ html | |||
| 		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') | ||||
| 		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') | ||||
| 		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') | ||||
| 		link(rel='preload' href='/assets/fontawesome/css/all.css' as='style') | ||||
| 		link(rel='stylesheet' href='/assets/fontawesome/css/all.css') | ||||
| 		link(rel='modulepreload' href=`/assets/${clientEntry.file}`) | ||||
| 
 | ||||
| 		each href in clientEntry.css | ||||
| 			link(rel='preload' href=`/assets/${href}` as='style') | ||||
| 
 | ||||
| 		each href in clientEntry.css | ||||
| 			link(rel='preload' href=`/assets/${href}` as='style') | ||||
| 
 | ||||
| 		title | ||||
| 			block title | ||||
|  | @ -52,7 +64,7 @@ html | |||
| 
 | ||||
| 		script. | ||||
| 			var VERSION = "#{version}"; | ||||
| 			var CLIENT_ENTRY = "#{clientEntry()}"; | ||||
| 			var CLIENT_ENTRY = "#{clientEntry.file}"; | ||||
| 
 | ||||
| 		script | ||||
| 			include ../boot.js | ||||
|  | @ -65,4 +77,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 | ||||
|  |  | |||
|  | @ -91,27 +91,20 @@ type ToJsonSchema<S> = { | |||
| }; | ||||
| 
 | ||||
| export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> { | ||||
| 	const object = {}; | ||||
| 	for (const [k, v] of Object.entries(schema)) { | ||||
| 		nestedProperty.set(object, k, null); | ||||
| 	} | ||||
| 	const jsonSchema = { | ||||
| 		type: 'object', | ||||
| 		properties: {} as Record<string, unknown>, | ||||
| 		required: [], | ||||
| 	}; | ||||
| 
 | ||||
| 	function f(obj: Record<string, null | Record<string, unknown>>) { | ||||
| 		const jsonSchema = { | ||||
| 			type: 'object', | ||||
| 			properties: {} as Record<string, unknown>, | ||||
| 			required: [], | ||||
| 	for (const k in schema) { | ||||
| 		jsonSchema.properties[k] = { | ||||
| 			type: 'array', | ||||
| 			items: { type: 'number' }, | ||||
| 		}; | ||||
| 		for (const [k, v] of Object.entries(obj)) { | ||||
| 			jsonSchema.properties[k] = v === null ? { | ||||
| 				type: 'array', | ||||
| 				items: { type: 'number' }, | ||||
| 			} : f(v as Record<string, null | Record<string, unknown>>); | ||||
| 		} | ||||
| 		return jsonSchema; | ||||
| 	} | ||||
| 
 | ||||
| 	return f(object) as ToJsonSchema<Unflatten<ChartResult<S>>>; | ||||
| 	return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
| 	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%'], | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	await new Promise((res, rej) => { | ||||
| 		FFmpeg({ | ||||
| 			source: path, | ||||
| 		}) | ||||
| 		.on('end', res) | ||||
| 		.on('error', rej) | ||||
| 		.screenshot({ | ||||
| 			folder: outDir, | ||||
| 			filename: 'output.png', | ||||
| 			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(); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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(); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| { | ||||
| 	"env": { | ||||
| 		"node": true, | ||||
| 		"mocha": true, | ||||
| 		"commonjs": true | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										11
									
								
								packages/backend/test/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/backend/test/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| module.exports = { | ||||
| 	parserOptions: { | ||||
| 		tsconfigRootDir: __dirname, | ||||
| 		project: ['./tsconfig.json'], | ||||
| 	}, | ||||
| 	extends: ['../.eslintrc.cjs'], | ||||
| 	env: { | ||||
| 		node: true, | ||||
| 		mocha: true, | ||||
| 	}, | ||||
| }; | ||||
|  | @ -1,7 +1,7 @@ | |||
| process.env.NODE_ENV = 'test'; | ||||
| 
 | ||||
| import rndstr from 'rndstr'; | ||||
| import * as assert from 'assert'; | ||||
| import rndstr from 'rndstr'; | ||||
| import { initTestDb } from './utils.js'; | ||||
| 
 | ||||
| describe('ActivityPub', () => { | ||||
|  | @ -57,8 +57,8 @@ describe('ActivityPub', () => { | |||
| 			const note = await createNote(post.id, resolver, true); | ||||
| 
 | ||||
| 			assert.deepStrictEqual(note?.uri, post.id); | ||||
| 			assert.deepStrictEqual(note?.visibility, 'public'); | ||||
| 			assert.deepStrictEqual(note?.text, post.content); | ||||
| 			assert.deepStrictEqual(note.visibility, 'public'); | ||||
| 			assert.deepStrictEqual(note.text, post.content); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as assert from 'assert'; | ||||
| import httpSignature from 'http-signature'; | ||||
| import { genRsaKeyPair } from '../src/misc/gen-key-pair.js'; | ||||
| import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js'; | ||||
| import httpSignature from 'http-signature'; | ||||
| 
 | ||||
| export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { | ||||
| 	return { | ||||
|  | @ -13,7 +13,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a | |||
| 			signature: signature, | ||||
| 		}, | ||||
| 		signingString: signingString, | ||||
| 		algorithm: algorithm?.toUpperCase(), | ||||
| 		algorithm: algorithm.toUpperCase(), | ||||
| 		keyId: 'KeyID',	// dummy, not used for verify
 | ||||
| 	}; | ||||
| }; | ||||
|  | @ -26,7 +26,7 @@ describe('ap-request', () => { | |||
| 		const activity = { a: 1 }; | ||||
| 		const body = JSON.stringify(activity); | ||||
| 		const headers = { | ||||
| 			'User-Agent': 'UA' | ||||
| 			'User-Agent': 'UA', | ||||
| 		}; | ||||
| 
 | ||||
| 		const req = createSignedPost({ key, url, body, additionalHeaders: headers }); | ||||
|  | @ -42,7 +42,7 @@ describe('ap-request', () => { | |||
| 		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; | ||||
| 		const url = 'https://example.com/outbox'; | ||||
| 		const headers = { | ||||
| 			'User-Agent': 'UA' | ||||
| 			'User-Agent': 'UA', | ||||
| 		}; | ||||
| 
 | ||||
| 		const req = createSignedGet({ key, url, additionalHeaders: headers }); | ||||
|  |  | |||
|  | @ -61,40 +61,40 @@ describe('API visibility', () => { | |||
| 
 | ||||
| 		const show = async (noteId: any, by: any) => { | ||||
| 			return await request('/notes/show', { | ||||
| 				noteId | ||||
| 				noteId, | ||||
| 			}, by); | ||||
| 		}; | ||||
| 
 | ||||
| 		before(async () => { | ||||
| 			//#region prepare
 | ||||
| 			// signup
 | ||||
| 			alice    = await signup({ username: 'alice' }); | ||||
| 			alice = await signup({ username: 'alice' }); | ||||
| 			follower = await signup({ username: 'follower' }); | ||||
| 			other    = await signup({ username: 'other' }); | ||||
| 			target   = await signup({ username: 'target' }); | ||||
| 			target2  = await signup({ username: 'target2' }); | ||||
| 			other = await signup({ username: 'other' }); | ||||
| 			target = await signup({ username: 'target' }); | ||||
| 			target2 = await signup({ username: 'target2' }); | ||||
| 
 | ||||
| 			// follow alice <= follower
 | ||||
| 			await request('/following/create', { userId: alice.id }, follower); | ||||
| 
 | ||||
| 			// normal posts
 | ||||
| 			pub  = await post(alice, { text: 'x', visibility: 'public' }); | ||||
| 			pub = await post(alice, { text: 'x', visibility: 'public' }); | ||||
| 			home = await post(alice, { text: 'x', visibility: 'home' }); | ||||
| 			fol  = await post(alice, { text: 'x', visibility: 'followers' }); | ||||
| 			spe  = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); | ||||
| 			fol = await post(alice, { text: 'x', visibility: 'followers' }); | ||||
| 			spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); | ||||
| 
 | ||||
| 			// replies
 | ||||
| 			tgt = await post(target, { text: 'y', visibility: 'public' }); | ||||
| 			pubR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); | ||||
| 			pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); | ||||
| 			homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); | ||||
| 			folR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); | ||||
| 			speR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); | ||||
| 			folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); | ||||
| 			speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); | ||||
| 
 | ||||
| 			// mentions
 | ||||
| 			pubM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); | ||||
| 			pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); | ||||
| 			homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); | ||||
| 			folM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); | ||||
| 			speM  = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); | ||||
| 			folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); | ||||
| 			speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); | ||||
| 			//#endregion
 | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ describe('Block', () => { | |||
| 
 | ||||
| 	it('Block作成', async(async () => { | ||||
| 		const res = await request('/blocking/create', { | ||||
| 			userId: bob.id | ||||
| 			userId: bob.id, | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as lolex from '@sinonjs/fake-timers'; | ||||
| import { async, initTestDb } from './utils.js'; | ||||
| import TestChart from '../src/services/chart/charts/test.js'; | ||||
| import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; | ||||
| import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; | ||||
|  | @ -11,6 +10,7 @@ import * as _TestChart from '../src/services/chart/charts/entities/test.js'; | |||
| import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js'; | ||||
| import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js'; | ||||
| import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js'; | ||||
| import { async, initTestDb } from './utils.js'; | ||||
| 
 | ||||
| describe('Chart', () => { | ||||
| 	let testChart: TestChart; | ||||
|  | @ -33,7 +33,7 @@ describe('Chart', () => { | |||
| 		testIntersectionChart = new TestIntersectionChart(); | ||||
| 
 | ||||
| 		clock = lolex.install({ | ||||
| 			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)) | ||||
| 			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), | ||||
| 		}); | ||||
| 	})); | ||||
| 
 | ||||
|  | @ -52,7 +52,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -60,7 +60,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -76,7 +76,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [1, 0, 0], | ||||
| 				inc: [0, 0, 0], | ||||
| 				total: [-1, 0, 0] | ||||
| 				total: [-1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -84,7 +84,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [1, 0, 0], | ||||
| 				inc: [0, 0, 0], | ||||
| 				total: [-1, 0, 0] | ||||
| 				total: [-1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -97,7 +97,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [0, 0, 0], | ||||
| 				total: [0, 0, 0] | ||||
| 				total: [0, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -105,7 +105,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [0, 0, 0], | ||||
| 				total: [0, 0, 0] | ||||
| 				total: [0, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -123,7 +123,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [3, 0, 0], | ||||
| 				total: [3, 0, 0] | ||||
| 				total: [3, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -131,7 +131,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [3, 0, 0], | ||||
| 				total: [3, 0, 0] | ||||
| 				total: [3, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -149,7 +149,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -157,7 +157,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -178,7 +178,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 1, 0], | ||||
| 				total: [2, 1, 0] | ||||
| 				total: [2, 1, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -186,7 +186,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [2, 0, 0], | ||||
| 				total: [2, 0, 0] | ||||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -238,7 +238,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 1], | ||||
| 				total: [2, 1, 1] | ||||
| 				total: [2, 1, 1], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -246,7 +246,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [2, 0, 0], | ||||
| 				total: [2, 0, 0] | ||||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -265,7 +265,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [0, 0, 0], | ||||
| 				total: [1, 1, 1] | ||||
| 				total: [1, 1, 1], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -273,7 +273,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -296,7 +296,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [2, 1, 1] | ||||
| 				total: [2, 1, 1], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -304,7 +304,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [2, 0, 0], | ||||
| 				total: [2, 0, 0] | ||||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -325,7 +325,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -333,7 +333,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [2, 0, 0], | ||||
| 				total: [2, 0, 0] | ||||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -356,7 +356,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [1, 0, 0], | ||||
| 				total: [1, 0, 0] | ||||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -364,7 +364,7 @@ describe('Chart', () => { | |||
| 			foo: { | ||||
| 				dec: [0, 0, 0], | ||||
| 				inc: [2, 0, 0], | ||||
| 				total: [2, 0, 0] | ||||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
|  | @ -383,7 +383,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [1, 0, 0], | ||||
| 					total: [1, 0, 0] | ||||
| 					total: [1, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -391,7 +391,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [1, 0, 0], | ||||
| 					total: [1, 0, 0] | ||||
| 					total: [1, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -399,7 +399,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [0, 0, 0], | ||||
| 					total: [0, 0, 0] | ||||
| 					total: [0, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -407,7 +407,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [0, 0, 0], | ||||
| 					total: [0, 0, 0] | ||||
| 					total: [0, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
|  | @ -493,7 +493,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [0, 0, 0], | ||||
| 					total: [1, 0, 0] | ||||
| 					total: [1, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -501,7 +501,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [0, 0, 0], | ||||
| 					total: [1, 0, 0] | ||||
| 					total: [1, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
|  | @ -523,7 +523,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [0, 1, 0], | ||||
| 					total: [100, 1, 0] | ||||
| 					total: [100, 1, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -531,7 +531,7 @@ describe('Chart', () => { | |||
| 				foo: { | ||||
| 					dec: [0, 0, 0], | ||||
| 					inc: [1, 0, 0], | ||||
| 					total: [100, 0, 0] | ||||
| 					total: [100, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as assert from 'assert'; | ||||
| 
 | ||||
| import { extractMentions } from '../src/misc/extract-mentions.js'; | ||||
| import { parse } from 'mfm-js'; | ||||
| import { extractMentions } from '../src/misc/extract-mentions.js'; | ||||
| 
 | ||||
| describe('Extract mentions', () => { | ||||
| 	it('simple', () => { | ||||
|  | @ -10,15 +10,15 @@ describe('Extract mentions', () => { | |||
| 		assert.deepStrictEqual(mentions, [{ | ||||
| 			username: 'foo', | ||||
| 			acct: '@foo', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}, { | ||||
| 			username: 'bar', | ||||
| 			acct: '@bar', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}, { | ||||
| 			username: 'baz', | ||||
| 			acct: '@baz', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}]); | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -28,15 +28,15 @@ describe('Extract mentions', () => { | |||
| 		assert.deepStrictEqual(mentions, [{ | ||||
| 			username: 'foo', | ||||
| 			acct: '@foo', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}, { | ||||
| 			username: 'bar', | ||||
| 			acct: '@bar', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}, { | ||||
| 			username: 'baz', | ||||
| 			acct: '@baz', | ||||
| 			host: null | ||||
| 			host: null, | ||||
| 		}]); | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; | ||||
| import * as openapi from '@redocly/openapi-core'; | ||||
| import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; | ||||
| 
 | ||||
| // Request Accept
 | ||||
| const ONLY_AP = 'application/activity+json'; | ||||
|  | @ -26,7 +26,7 @@ describe('Fetch resource', () => { | |||
| 		p = await startServer(); | ||||
| 		alice = await signup({ username: 'alice' }); | ||||
| 		alicesPost = await post(alice, { | ||||
| 			text: 'test' | ||||
| 			text: 'test', | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -70,7 +70,7 @@ describe('Fetch resource', () => { | |||
| 			const config = await openapi.loadConfig(); | ||||
| 			const result = await openapi.bundle({ | ||||
| 				config, | ||||
| 				ref: `http://localhost:${port}/api.json` | ||||
| 				ref: `http://localhost:${port}/api.json`, | ||||
| 			}); | ||||
| 
 | ||||
| 			for (const problem of result.problems) { | ||||
|  |  | |||
|  | @ -1,10 +1,15 @@ | |||
| import * as assert from 'assert'; | ||||
| import { async } from './utils.js'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import { dirname } from 'node:path'; | ||||
| import { getFileInfo } from '../src/misc/get-file-info.js'; | ||||
| import { async } from './utils.js'; | ||||
| 
 | ||||
| const _filename = fileURLToPath(import.meta.url); | ||||
| const _dirname = dirname(_filename); | ||||
| 
 | ||||
| describe('Get file info', () => { | ||||
| 	it('Empty file', async (async () => { | ||||
| 		const path = `${__dirname}/resources/emptyfile`; | ||||
| 		const path = `${_dirname}/resources/emptyfile`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -13,7 +18,7 @@ describe('Get file info', () => { | |||
| 			md5: 'd41d8cd98f00b204e9800998ecf8427e', | ||||
| 			type: { | ||||
| 				mime: 'application/octet-stream', | ||||
| 				ext: null | ||||
| 				ext: null, | ||||
| 			}, | ||||
| 			width: undefined, | ||||
| 			height: undefined, | ||||
|  | @ -22,7 +27,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Generic JPEG', async (async () => { | ||||
| 		const path = `${__dirname}/resources/Lenna.jpg`; | ||||
| 		const path = `${_dirname}/resources/Lenna.jpg`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -31,7 +36,7 @@ describe('Get file info', () => { | |||
| 			md5: '091b3f259662aa31e2ffef4519951168', | ||||
| 			type: { | ||||
| 				mime: 'image/jpeg', | ||||
| 				ext: 'jpg' | ||||
| 				ext: 'jpg', | ||||
| 			}, | ||||
| 			width: 512, | ||||
| 			height: 512, | ||||
|  | @ -40,7 +45,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Generic APNG', async (async () => { | ||||
| 		const path = `${__dirname}/resources/anime.png`; | ||||
| 		const path = `${_dirname}/resources/anime.png`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -49,7 +54,7 @@ describe('Get file info', () => { | |||
| 			md5: '08189c607bea3b952704676bb3c979e0', | ||||
| 			type: { | ||||
| 				mime: 'image/apng', | ||||
| 				ext: 'apng' | ||||
| 				ext: 'apng', | ||||
| 			}, | ||||
| 			width: 256, | ||||
| 			height: 256, | ||||
|  | @ -58,7 +63,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Generic AGIF', async (async () => { | ||||
| 		const path = `${__dirname}/resources/anime.gif`; | ||||
| 		const path = `${_dirname}/resources/anime.gif`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -67,7 +72,7 @@ describe('Get file info', () => { | |||
| 			md5: '32c47a11555675d9267aee1a86571e7e', | ||||
| 			type: { | ||||
| 				mime: 'image/gif', | ||||
| 				ext: 'gif' | ||||
| 				ext: 'gif', | ||||
| 			}, | ||||
| 			width: 256, | ||||
| 			height: 256, | ||||
|  | @ -76,7 +81,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('PNG with alpha', async (async () => { | ||||
| 		const path = `${__dirname}/resources/with-alpha.png`; | ||||
| 		const path = `${_dirname}/resources/with-alpha.png`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -85,7 +90,7 @@ describe('Get file info', () => { | |||
| 			md5: 'f73535c3e1e27508885b69b10cf6e991', | ||||
| 			type: { | ||||
| 				mime: 'image/png', | ||||
| 				ext: 'png' | ||||
| 				ext: 'png', | ||||
| 			}, | ||||
| 			width: 256, | ||||
| 			height: 256, | ||||
|  | @ -94,7 +99,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Generic SVG', async (async () => { | ||||
| 		const path = `${__dirname}/resources/image.svg`; | ||||
| 		const path = `${_dirname}/resources/image.svg`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -103,7 +108,7 @@ describe('Get file info', () => { | |||
| 			md5: 'b6f52b4b021e7b92cdd04509c7267965', | ||||
| 			type: { | ||||
| 				mime: 'image/svg+xml', | ||||
| 				ext: 'svg' | ||||
| 				ext: 'svg', | ||||
| 			}, | ||||
| 			width: 256, | ||||
| 			height: 256, | ||||
|  | @ -113,7 +118,7 @@ describe('Get file info', () => { | |||
| 
 | ||||
| 	it('SVG with XML definition', async (async () => { | ||||
| 		// https://github.com/misskey-dev/misskey/issues/4413
 | ||||
| 		const path = `${__dirname}/resources/with-xml-def.svg`; | ||||
| 		const path = `${_dirname}/resources/with-xml-def.svg`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -122,7 +127,7 @@ describe('Get file info', () => { | |||
| 			md5: '4b7a346cde9ccbeb267e812567e33397', | ||||
| 			type: { | ||||
| 				mime: 'image/svg+xml', | ||||
| 				ext: 'svg' | ||||
| 				ext: 'svg', | ||||
| 			}, | ||||
| 			width: 256, | ||||
| 			height: 256, | ||||
|  | @ -131,7 +136,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Dimension limit', async (async () => { | ||||
| 		const path = `${__dirname}/resources/25000x25000.png`; | ||||
| 		const path = `${_dirname}/resources/25000x25000.png`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -140,7 +145,7 @@ describe('Get file info', () => { | |||
| 			md5: '268c5dde99e17cf8fe09f1ab3f97df56', | ||||
| 			type: { | ||||
| 				mime: 'application/octet-stream',	// do not treat as image
 | ||||
| 				ext: null | ||||
| 				ext: null, | ||||
| 			}, | ||||
| 			width: 25000, | ||||
| 			height: 25000, | ||||
|  | @ -149,7 +154,7 @@ describe('Get file info', () => { | |||
| 	})); | ||||
| 
 | ||||
| 	it('Rotate JPEG', async (async () => { | ||||
| 		const path = `${__dirname}/resources/rotate.jpg`; | ||||
| 		const path = `${_dirname}/resources/rotate.jpg`; | ||||
| 		const info = await getFileInfo(path) as any; | ||||
| 		delete info.warnings; | ||||
| 		delete info.blurhash; | ||||
|  | @ -158,7 +163,7 @@ describe('Get file info', () => { | |||
| 			md5: '68d5b2d8d1d1acbbce99203e3ec3857e', | ||||
| 			type: { | ||||
| 				mime: 'image/jpeg', | ||||
| 				ext: 'jpg' | ||||
| 				ext: 'jpg', | ||||
| 			}, | ||||
| 			width: 512, | ||||
| 			height: 256, | ||||
|  |  | |||
|  | @ -1,37 +1,34 @@ | |||
| import path from 'path' | ||||
| import typescript from 'typescript' | ||||
| import { createMatchPath } from 'tsconfig-paths' | ||||
| import { resolve as BaseResolve, getFormat, transformSource } from 'ts-node/esm' | ||||
| /** | ||||
|  * ts-node/esmローダーに投げる前にpath mappingを解決する | ||||
|  * 参考 | ||||
|  * - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
 | ||||
|  * - https://nodejs.org/api/esm.html#loaders
 | ||||
|  * ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる
 | ||||
|  */ | ||||
| 
 | ||||
| const { readConfigFile, parseJsonConfigFileContent, sys } = typescript | ||||
| import { resolve as resolveTs, load } from 'ts-node/esm'; | ||||
| import { loadConfig, createMatchPath } from 'tsconfig-paths'; | ||||
| import { pathToFileURL } from 'url'; | ||||
| 
 | ||||
| const __dirname = path.dirname(new URL(import.meta.url).pathname) | ||||
| const tsconfig = loadConfig(); | ||||
| const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths); | ||||
| 
 | ||||
| const configFile = readConfigFile('./test/tsconfig.json', sys.readFile) | ||||
| if (typeof configFile.error !== 'undefined') { | ||||
|   throw new Error(`Failed to load tsconfig: ${configFile.error}`) | ||||
| export function resolve(specifier, ctx, defaultResolve) { | ||||
| 	let resolvedSpecifier; | ||||
| 	if (specifier.endsWith('.js')) { | ||||
| 		// maybe transpiled
 | ||||
| 		const specifierWithoutExtension = specifier.substring(0, specifier.length - '.js'.length); | ||||
| 		const matchedSpecifier = matchPath(specifierWithoutExtension); | ||||
| 		if (matchedSpecifier) { | ||||
| 			resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href; | ||||
| 		} | ||||
| 	} else { | ||||
| 		const matchedSpecifier = matchPath(specifier); | ||||
| 		if (matchedSpecifier) { | ||||
| 			resolvedSpecifier = pathToFileURL(matchedSpecifier).href; | ||||
| 		} | ||||
| 	} | ||||
| 	return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve); | ||||
| } | ||||
| 
 | ||||
| const { options } = parseJsonConfigFileContent( | ||||
|   configFile.config, | ||||
|   { | ||||
|     fileExists: sys.fileExists, | ||||
|     readFile: sys.readFile, | ||||
|     readDirectory: sys.readDirectory, | ||||
|     useCaseSensitiveFileNames: true, | ||||
|   }, | ||||
|   __dirname | ||||
| ) | ||||
| 
 | ||||
| export { getFormat, transformSource }  // こいつらはそのまま使ってほしいので re-export する
 | ||||
| 
 | ||||
| const matchPath = createMatchPath(options.baseUrl, options.paths) | ||||
| 
 | ||||
| export async function resolve(specifier, context, defaultResolve) { | ||||
|   const matchedSpecifier = matchPath(specifier.replace('.js', '.ts')) | ||||
|   return BaseResolve(  // ts-node/esm の resolve に tsconfig-paths で解決したパスを渡す
 | ||||
|     matchedSpecifier ? `${matchedSpecifier}.ts` : specifier, | ||||
|     context, | ||||
|     defaultResolve | ||||
|   ) | ||||
| } | ||||
| export { load }; | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ export class MockResolver extends Resolver { | |||
| 	public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') { | ||||
| 		this._rs.set(uri, { | ||||
| 			type, | ||||
| 			content: typeof content === 'string' ? content : JSON.stringify(content) | ||||
| 			content: typeof content === 'string' ? content : JSON.stringify(content), | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -22,9 +22,9 @@ export class MockResolver extends Resolver { | |||
| 
 | ||||
| 		if (!r) { | ||||
| 			throw { | ||||
| 				name: `StatusError`, | ||||
| 				name: 'StatusError', | ||||
| 				statusCode: 404, | ||||
| 				message: `Not registed for mock` | ||||
| 				message: 'Not registed for mock', | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ describe('Mute', () => { | |||
| 
 | ||||
| 	it('ミュート作成', async(async () => { | ||||
| 		const res = await request('/mute/create', { | ||||
| 			userId: carol.id | ||||
| 			userId: carol.id, | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 204); | ||||
|  | @ -117,7 +117,7 @@ describe('Mute', () => { | |||
| 			const aliceNote = await post(alice); | ||||
| 			const carolNote = await post(carol); | ||||
| 			const bobNote = await post(bob, { | ||||
| 				renoteId: carolNote.id | ||||
| 				renoteId: carolNote.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			const res = await request('/notes/local-timeline', {}, alice); | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js'; | ||||
| import { Note } from '../src/models/entities/note.js'; | ||||
| import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js'; | ||||
| 
 | ||||
| describe('Note', () => { | ||||
| 	let p: childProcess.ChildProcess; | ||||
|  | @ -26,7 +26,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('投稿できる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: 'test' | ||||
| 			text: 'test', | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
|  | @ -40,7 +40,7 @@ describe('Note', () => { | |||
| 		const file = await uploadFile(alice); | ||||
| 
 | ||||
| 		const res = await request('/notes/create', { | ||||
| 			fileIds: [file.id] | ||||
| 			fileIds: [file.id], | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  | @ -53,7 +53,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 		const res = await request('/notes/create', { | ||||
| 			text: 'test', | ||||
| 			fileIds: [file.id] | ||||
| 			fileIds: [file.id], | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  | @ -64,7 +64,7 @@ describe('Note', () => { | |||
| 	it('存在しないファイルは無視', async(async () => { | ||||
| 		const res = await request('/notes/create', { | ||||
| 			text: 'test', | ||||
| 			fileIds: ['000000000000000000000000'] | ||||
| 			fileIds: ['000000000000000000000000'], | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  | @ -74,19 +74,19 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('不正なファイルIDで怒られる', async(async () => { | ||||
| 		const res = await request('/notes/create', { | ||||
| 			fileIds: ['kyoppie'] | ||||
| 			fileIds: ['kyoppie'], | ||||
| 		}, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
| 	})); | ||||
| 
 | ||||
| 	it('返信できる', async(async () => { | ||||
| 		const bobPost = await post(bob, { | ||||
| 			text: 'foo' | ||||
| 			text: 'foo', | ||||
| 		}); | ||||
| 
 | ||||
| 		const alicePost = { | ||||
| 			text: 'bar', | ||||
| 			replyId: bobPost.id | ||||
| 			replyId: bobPost.id, | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', alicePost, alice); | ||||
|  | @ -100,11 +100,11 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('renoteできる', async(async () => { | ||||
| 		const bobPost = await post(bob, { | ||||
| 			text: 'test' | ||||
| 			text: 'test', | ||||
| 		}); | ||||
| 
 | ||||
| 		const alicePost = { | ||||
| 			renoteId: bobPost.id | ||||
| 			renoteId: bobPost.id, | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', alicePost, alice); | ||||
|  | @ -117,12 +117,12 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('引用renoteできる', async(async () => { | ||||
| 		const bobPost = await post(bob, { | ||||
| 			text: 'test' | ||||
| 			text: 'test', | ||||
| 		}); | ||||
| 
 | ||||
| 		const alicePost = { | ||||
| 			text: 'test', | ||||
| 			renoteId: bobPost.id | ||||
| 			renoteId: bobPost.id, | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', alicePost, alice); | ||||
|  | @ -136,7 +136,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('文字数ぎりぎりで怒られない', async(async () => { | ||||
| 		const post = { | ||||
| 			text: '!'.repeat(500) | ||||
| 			text: '!'.repeat(500), | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  | @ -144,7 +144,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('文字数オーバーで怒られる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: '!'.repeat(501) | ||||
| 			text: '!'.repeat(501), | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
|  | @ -153,7 +153,7 @@ describe('Note', () => { | |||
| 	it('存在しないリプライ先で怒られる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: 'test', | ||||
| 			replyId: '000000000000000000000000' | ||||
| 			replyId: '000000000000000000000000', | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
|  | @ -161,7 +161,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('存在しないrenote対象で怒られる', async(async () => { | ||||
| 		const post = { | ||||
| 			renoteId: '000000000000000000000000' | ||||
| 			renoteId: '000000000000000000000000', | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
|  | @ -170,7 +170,7 @@ describe('Note', () => { | |||
| 	it('不正なリプライ先IDで怒られる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: 'test', | ||||
| 			replyId: 'foo' | ||||
| 			replyId: 'foo', | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
|  | @ -178,7 +178,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('不正なrenote対象IDで怒られる', async(async () => { | ||||
| 		const post = { | ||||
| 			renoteId: 'foo' | ||||
| 			renoteId: 'foo', | ||||
| 		}; | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
| 		assert.strictEqual(res.status, 400); | ||||
|  | @ -186,7 +186,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('存在しないユーザーにメンションできる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: '@ghost yo' | ||||
| 			text: '@ghost yo', | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
|  | @ -198,7 +198,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 	it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => { | ||||
| 		const post = { | ||||
| 			text: '@bob @bob @bob yo' | ||||
| 			text: '@bob @bob @bob yo', | ||||
| 		}; | ||||
| 
 | ||||
| 		const res = await request('/notes/create', post, alice); | ||||
|  | @ -216,8 +216,8 @@ describe('Note', () => { | |||
| 			const res = await request('/notes/create', { | ||||
| 				text: 'test', | ||||
| 				poll: { | ||||
| 					choices: ['foo', 'bar'] | ||||
| 				} | ||||
| 					choices: ['foo', 'bar'], | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.status, 200); | ||||
|  | @ -227,7 +227,7 @@ describe('Note', () => { | |||
| 
 | ||||
| 		it('投票の選択肢が無くて怒られる', async(async () => { | ||||
| 			const res = await request('/notes/create', { | ||||
| 				poll: {} | ||||
| 				poll: {}, | ||||
| 			}, alice); | ||||
| 			assert.strictEqual(res.status, 400); | ||||
| 		})); | ||||
|  | @ -235,8 +235,8 @@ describe('Note', () => { | |||
| 		it('投票の選択肢が無くて怒られる (空の配列)', async(async () => { | ||||
| 			const res = await request('/notes/create', { | ||||
| 				poll: { | ||||
| 					choices: [] | ||||
| 				} | ||||
| 					choices: [], | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 			assert.strictEqual(res.status, 400); | ||||
| 		})); | ||||
|  | @ -244,8 +244,8 @@ describe('Note', () => { | |||
| 		it('投票の選択肢が1つで怒られる', async(async () => { | ||||
| 			const res = await request('/notes/create', { | ||||
| 				poll: { | ||||
| 					choices: ['Strawberry Pasta'] | ||||
| 				} | ||||
| 					choices: ['Strawberry Pasta'], | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 			assert.strictEqual(res.status, 400); | ||||
| 		})); | ||||
|  | @ -254,13 +254,13 @@ describe('Note', () => { | |||
| 			const { body } = await request('/notes/create', { | ||||
| 				text: 'test', | ||||
| 				poll: { | ||||
| 					choices: ['sakura', 'izumi', 'ako'] | ||||
| 				} | ||||
| 					choices: ['sakura', 'izumi', 'ako'], | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const res = await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 1 | ||||
| 				choice: 1, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.status, 204); | ||||
|  | @ -270,18 +270,18 @@ describe('Note', () => { | |||
| 			const { body } = await request('/notes/create', { | ||||
| 				text: 'test', | ||||
| 				poll: { | ||||
| 					choices: ['sakura', 'izumi', 'ako'] | ||||
| 				} | ||||
| 					choices: ['sakura', 'izumi', 'ako'], | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 0 | ||||
| 				choice: 0, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const res = await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 2 | ||||
| 				choice: 2, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.status, 400); | ||||
|  | @ -292,23 +292,23 @@ describe('Note', () => { | |||
| 				text: 'test', | ||||
| 				poll: { | ||||
| 					choices: ['sakura', 'izumi', 'ako'], | ||||
| 					multiple: true | ||||
| 				} | ||||
| 					multiple: true, | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 0 | ||||
| 				choice: 0, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 1 | ||||
| 				choice: 1, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const res = await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 2 | ||||
| 				choice: 2, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.status, 204); | ||||
|  | @ -319,15 +319,15 @@ describe('Note', () => { | |||
| 				text: 'test', | ||||
| 				poll: { | ||||
| 					choices: ['sakura', 'izumi', 'ako'], | ||||
| 					expiredAfter: 1 | ||||
| 				} | ||||
| 					expiredAfter: 1, | ||||
| 				}, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			await new Promise(x => setTimeout(x, 2)); | ||||
| 
 | ||||
| 			const res = await request('/notes/polls/vote', { | ||||
| 				noteId: body.createdNote.id, | ||||
| 				choice: 1 | ||||
| 				choice: 1, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.status, 400); | ||||
|  | @ -341,11 +341,11 @@ describe('Note', () => { | |||
| 			}, alice); | ||||
| 			const replyOneRes = await request('/notes/create', { | ||||
| 				text: 'reply one', | ||||
| 				replyId: mainNoteRes.body.createdNote.id | ||||
| 				replyId: mainNoteRes.body.createdNote.id, | ||||
| 			}, alice); | ||||
| 			const replyTwoRes = await request('/notes/create', { | ||||
| 				text: 'reply two', | ||||
| 				replyId: mainNoteRes.body.createdNote.id | ||||
| 				replyId: mainNoteRes.body.createdNote.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const deleteOneRes = await request('/notes/delete', { | ||||
|  | @ -353,7 +353,7 @@ describe('Note', () => { | |||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(deleteOneRes.status, 204); | ||||
| 			let mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id}); | ||||
| 			let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id }); | ||||
| 			assert.strictEqual(mainNote.repliesCount, 1); | ||||
| 
 | ||||
| 			const deleteTwoRes = await request('/notes/delete', { | ||||
|  | @ -361,7 +361,7 @@ describe('Note', () => { | |||
| 			}, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(deleteTwoRes.status, 204); | ||||
| 			mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id}); | ||||
| 			mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id }); | ||||
| 			assert.strictEqual(mainNote.repliesCount, 0); | ||||
| 		})); | ||||
| 	}); | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ describe('url', () => { | |||
| 		const s = query({ | ||||
| 			foo: 'ふぅ', | ||||
| 			bar: 'b a r', | ||||
| 			baz: undefined | ||||
| 			baz: undefined, | ||||
| 		}); | ||||
| 		assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r'); | ||||
| 	}); | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js'; | ||||
| import { Following } from '../src/models/entities/following.js'; | ||||
| import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js'; | ||||
| 
 | ||||
| describe('Streaming', () => { | ||||
| 	let p: childProcess.ChildProcess; | ||||
|  | @ -30,7 +30,7 @@ describe('Streaming', () => { | |||
| 			followerSharedInbox: null, | ||||
| 			followeeHost: followee.host, | ||||
| 			followeeInbox: null, | ||||
| 			followeeSharedInbox: null | ||||
| 			followeeSharedInbox: null, | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -47,7 +47,7 @@ describe('Streaming', () => { | |||
| 		}); | ||||
| 
 | ||||
| 		post(alice, { | ||||
| 			text: 'foo @bob bar' | ||||
| 			text: 'foo @bob bar', | ||||
| 		}); | ||||
| 	})); | ||||
| 
 | ||||
|  | @ -55,7 +55,7 @@ describe('Streaming', () => { | |||
| 		const alice = await signup({ username: 'alice' }); | ||||
| 		const bob = await signup({ username: 'bob' }); | ||||
| 		const bobNote = await post(bob, { | ||||
| 			text: 'foo' | ||||
| 			text: 'foo', | ||||
| 		}); | ||||
| 
 | ||||
| 		const ws = await connectStream(bob, 'main', ({ type, body }) => { | ||||
|  | @ -67,14 +67,14 @@ describe('Streaming', () => { | |||
| 		}); | ||||
| 
 | ||||
| 		post(alice, { | ||||
| 			renoteId: bobNote.id | ||||
| 			renoteId: bobNote.id, | ||||
| 		}); | ||||
| 	})); | ||||
| 
 | ||||
| 	describe('Home Timeline', () => { | ||||
| 		it('自分の投稿が流れる', () => new Promise(async done => { | ||||
| 			const post = { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}; | ||||
| 
 | ||||
| 			const me = await signup(); | ||||
|  | @ -96,7 +96,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { | ||||
|  | @ -108,7 +108,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -125,7 +125,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -141,7 +141,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { | ||||
|  | @ -157,7 +157,7 @@ describe('Streaming', () => { | |||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'specified', | ||||
| 				visibleUserIds: [alice.id] | ||||
| 				visibleUserIds: [alice.id], | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -168,7 +168,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			let fired = false; | ||||
|  | @ -183,7 +183,7 @@ describe('Streaming', () => { | |||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'specified', | ||||
| 				visibleUserIds: [carol.id] | ||||
| 				visibleUserIds: [carol.id], | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -207,7 +207,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -224,7 +224,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -241,7 +241,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -257,7 +257,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			let fired = false; | ||||
|  | @ -269,7 +269,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -294,7 +294,7 @@ describe('Streaming', () => { | |||
| 			// ホーム指定
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'home' | ||||
| 				visibility: 'home', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -310,7 +310,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			let fired = false; | ||||
|  | @ -325,7 +325,7 @@ describe('Streaming', () => { | |||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'specified', | ||||
| 				visibleUserIds: [alice.id] | ||||
| 				visibleUserIds: [alice.id], | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -350,7 +350,7 @@ describe('Streaming', () => { | |||
| 			// フォロワー宛て投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'followers' | ||||
| 				visibility: 'followers', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -374,7 +374,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -391,7 +391,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -411,7 +411,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -428,7 +428,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -444,7 +444,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { | ||||
|  | @ -460,7 +460,7 @@ describe('Streaming', () => { | |||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'specified', | ||||
| 				visibleUserIds: [alice.id] | ||||
| 				visibleUserIds: [alice.id], | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -470,7 +470,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// Alice が Bob をフォロー
 | ||||
| 			await request('/following/create', { | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { | ||||
|  | @ -485,7 +485,7 @@ describe('Streaming', () => { | |||
| 			// ホーム投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'home' | ||||
| 				visibility: 'home', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -504,7 +504,7 @@ describe('Streaming', () => { | |||
| 			// ホーム投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'home' | ||||
| 				visibility: 'home', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -529,7 +529,7 @@ describe('Streaming', () => { | |||
| 			// フォロワー宛て投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'followers' | ||||
| 				visibility: 'followers', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -554,7 +554,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -571,7 +571,7 @@ describe('Streaming', () => { | |||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -590,7 +590,7 @@ describe('Streaming', () => { | |||
| 			// ホーム投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'home' | ||||
| 				visibility: 'home', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -608,13 +608,13 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// リスト作成
 | ||||
| 			const list = await request('/users/lists/create', { | ||||
| 				name: 'my list' | ||||
| 				name: 'my list', | ||||
| 			}, alice).then(x => x.body); | ||||
| 
 | ||||
| 			// Alice が Bob をリスイン
 | ||||
| 			await request('/users/lists/push', { | ||||
| 				listId: list.id, | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'userList', ({ type, body }) => { | ||||
|  | @ -624,11 +624,11 @@ describe('Streaming', () => { | |||
| 					done(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				listId: list.id | ||||
| 				listId: list.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -638,7 +638,7 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// リスト作成
 | ||||
| 			const list = await request('/users/lists/create', { | ||||
| 				name: 'my list' | ||||
| 				name: 'my list', | ||||
| 			}, alice).then(x => x.body); | ||||
| 
 | ||||
| 			let fired = false; | ||||
|  | @ -648,11 +648,11 @@ describe('Streaming', () => { | |||
| 					fired = true; | ||||
| 				} | ||||
| 			}, { | ||||
| 				listId: list.id | ||||
| 				listId: list.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo' | ||||
| 				text: 'foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -669,13 +669,13 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// リスト作成
 | ||||
| 			const list = await request('/users/lists/create', { | ||||
| 				name: 'my list' | ||||
| 				name: 'my list', | ||||
| 			}, alice).then(x => x.body); | ||||
| 
 | ||||
| 			// Alice が Bob をリスイン
 | ||||
| 			await request('/users/lists/push', { | ||||
| 				listId: list.id, | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			const ws = await connectStream(alice, 'userList', ({ type, body }) => { | ||||
|  | @ -686,14 +686,14 @@ describe('Streaming', () => { | |||
| 					done(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				listId: list.id | ||||
| 				listId: list.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			// Bob が Alice 宛てのダイレクト投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'specified', | ||||
| 				visibleUserIds: [alice.id] | ||||
| 				visibleUserIds: [alice.id], | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -704,13 +704,13 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 			// リスト作成
 | ||||
| 			const list = await request('/users/lists/create', { | ||||
| 				name: 'my list' | ||||
| 				name: 'my list', | ||||
| 			}, alice).then(x => x.body); | ||||
| 
 | ||||
| 			// Alice が Bob をリスイン
 | ||||
| 			await request('/users/lists/push', { | ||||
| 				listId: list.id, | ||||
| 				userId: bob.id | ||||
| 				userId: bob.id, | ||||
| 			}, alice); | ||||
| 
 | ||||
| 			let fired = false; | ||||
|  | @ -720,13 +720,13 @@ describe('Streaming', () => { | |||
| 					fired = true; | ||||
| 				} | ||||
| 			}, { | ||||
| 				listId: list.id | ||||
| 				listId: list.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			// フォロワー宛て投稿
 | ||||
| 			post(bob, { | ||||
| 				text: 'foo', | ||||
| 				visibility: 'followers' | ||||
| 				visibility: 'followers', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -749,12 +749,12 @@ describe('Streaming', () => { | |||
| 				} | ||||
| 			}, { | ||||
| 				q: [ | ||||
| 					['foo'] | ||||
| 				] | ||||
| 					['foo'], | ||||
| 				], | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo' | ||||
| 				text: '#foo', | ||||
| 			}); | ||||
| 		})); | ||||
| 
 | ||||
|  | @ -773,20 +773,20 @@ describe('Streaming', () => { | |||
| 				} | ||||
| 			}, { | ||||
| 				q: [ | ||||
| 					['foo', 'bar'] | ||||
| 				] | ||||
| 					['foo', 'bar'], | ||||
| 				], | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo' | ||||
| 				text: '#foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#bar' | ||||
| 				text: '#bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo #bar' | ||||
| 				text: '#foo #bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -816,24 +816,24 @@ describe('Streaming', () => { | |||
| 			}, { | ||||
| 				q: [ | ||||
| 					['foo'], | ||||
| 					['bar'] | ||||
| 				] | ||||
| 					['bar'], | ||||
| 				], | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo' | ||||
| 				text: '#foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#bar' | ||||
| 				text: '#bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo #bar' | ||||
| 				text: '#foo #bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#piyo' | ||||
| 				text: '#piyo', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  | @ -866,28 +866,28 @@ describe('Streaming', () => { | |||
| 			}, { | ||||
| 				q: [ | ||||
| 					['foo', 'bar'], | ||||
| 					['piyo'] | ||||
| 				] | ||||
| 					['piyo'], | ||||
| 				], | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo' | ||||
| 				text: '#foo', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#bar' | ||||
| 				text: '#bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#foo #bar' | ||||
| 				text: '#foo #bar', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#piyo' | ||||
| 				text: '#piyo', | ||||
| 			}); | ||||
| 
 | ||||
| 			post(me, { | ||||
| 				text: '#waaa' | ||||
| 				text: '#waaa', | ||||
| 			}); | ||||
| 
 | ||||
| 			setTimeout(() => { | ||||
|  |  | |||
|  | @ -2,8 +2,13 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import { dirname } from 'node:path'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils.js'; | ||||
| 
 | ||||
| const _filename = fileURLToPath(import.meta.url); | ||||
| const _dirname = dirname(_filename); | ||||
| 
 | ||||
| describe('users/notes', () => { | ||||
| 	let p: childProcess.ChildProcess; | ||||
| 
 | ||||
|  | @ -15,16 +20,16 @@ describe('users/notes', () => { | |||
| 	before(async () => { | ||||
| 		p = await startServer(); | ||||
| 		alice = await signup({ username: 'alice' }); | ||||
| 		const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg'); | ||||
| 		const png = await uploadFile(alice, __dirname + '/resources/Lenna.png'); | ||||
| 		const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg'); | ||||
| 		const png = await uploadFile(alice, _dirname + '/resources/Lenna.png'); | ||||
| 		jpgNote = await post(alice, { | ||||
| 			fileIds: [jpg.id] | ||||
| 			fileIds: [jpg.id], | ||||
| 		}); | ||||
| 		pngNote = await post(alice, { | ||||
| 			fileIds: [png.id] | ||||
| 			fileIds: [png.id], | ||||
| 		}); | ||||
| 		jpgPngNote = await post(alice, { | ||||
| 			fileIds: [jpg.id, png.id] | ||||
| 			fileIds: [jpg.id, png.id], | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -35,7 +40,7 @@ describe('users/notes', () => { | |||
| 	it('ファイルタイプ指定 (jpg)', async(async () => { | ||||
| 		const res = await request('/users/notes', { | ||||
| 			userId: alice.id, | ||||
| 			fileType: ['image/jpeg'] | ||||
| 			fileType: ['image/jpeg'], | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  | @ -48,7 +53,7 @@ describe('users/notes', () => { | |||
| 	it('ファイルタイプ指定 (jpg or png)', async(async () => { | ||||
| 		const res = await request('/users/notes', { | ||||
| 			userId: alice.id, | ||||
| 			fileType: ['image/jpeg', 'image/png'] | ||||
| 			fileType: ['image/jpeg', 'image/png'], | ||||
| 		}, alice); | ||||
| 
 | ||||
| 		assert.strictEqual(res.status, 200); | ||||
|  |  | |||
|  | @ -1,14 +1,20 @@ | |||
| import * as fs from 'node:fs'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import { dirname } from 'node:path'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import * as http from 'node:http'; | ||||
| import { SIGKILL } from 'constants'; | ||||
| import * as WebSocket from 'ws'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import fetch from 'node-fetch'; | ||||
| import FormData from 'form-data'; | ||||
| import * as childProcess from 'child_process'; | ||||
| import * as http from 'node:http'; | ||||
| import { DataSource } from 'typeorm'; | ||||
| import loadConfig from '../src/config/load.js'; | ||||
| import { SIGKILL } from 'constants'; | ||||
| import { entities } from '../src/db/postgre.js'; | ||||
| 
 | ||||
| const _filename = fileURLToPath(import.meta.url); | ||||
| const _dirname = dirname(_filename); | ||||
| 
 | ||||
| const config = loadConfig(); | ||||
| export const port = config.port; | ||||
| 
 | ||||
|  | @ -22,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => { | |||
| 
 | ||||
| export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { | ||||
| 	const auth = me ? { | ||||
| 		i: me.token | ||||
| 		i: me.token, | ||||
| 	} : {}; | ||||
| 
 | ||||
| 	const res = await fetch(`http://localhost:${port}/api${endpoint}`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json' | ||||
| 			'Content-Type': 'application/json', | ||||
| 		}, | ||||
| 		body: JSON.stringify(Object.assign(auth, params)) | ||||
| 		body: JSON.stringify(Object.assign(auth, params)), | ||||
| 	}); | ||||
| 
 | ||||
| 	const status = res.status; | ||||
| 	const body = res.status !== 204 ? await res.json().catch() : null; | ||||
| 
 | ||||
| 	return { | ||||
| 		body, status | ||||
| 		body, status, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| export const signup = async (params?: any): Promise<any> => { | ||||
| 	const q = Object.assign({ | ||||
| 		username: 'test', | ||||
| 		password: 'test' | ||||
| 		password: 'test', | ||||
| 	}, params); | ||||
| 
 | ||||
| 	const res = await request('/signup', q); | ||||
|  | @ -54,7 +60,7 @@ export const signup = async (params?: any): Promise<any> => { | |||
| 
 | ||||
| export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { | ||||
| 	const q = Object.assign({ | ||||
| 		text: 'test' | ||||
| 		text: 'test', | ||||
| 	}, params); | ||||
| 
 | ||||
| 	const res = await request('/notes/create', q, user); | ||||
|  | @ -65,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create'] | |||
| export const react = async (user: any, note: any, reaction: string): Promise<any> => { | ||||
| 	await request('/notes/reactions/create', { | ||||
| 		noteId: note.id, | ||||
| 		reaction: reaction | ||||
| 		reaction: reaction, | ||||
| 	}, user); | ||||
| }; | ||||
| 
 | ||||
| export const uploadFile = (user: any, path?: string): Promise<any> => { | ||||
| 		const formData = new FormData(); | ||||
| 		formData.append('i', user.token); | ||||
| 		formData.append('file', fs.createReadStream(path || __dirname + '/resources/Lenna.png')); | ||||
| 	const formData = new FormData(); | ||||
| 	formData.append('i', user.token); | ||||
| 	formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png')); | ||||
| 
 | ||||
| 		return fetch(`http://localhost:${port}/api/drive/files/create`, { | ||||
| 			method: 'post', | ||||
| 			body: formData, | ||||
| 			timeout: 30 * 1000, | ||||
| 		}).then(res => { | ||||
| 			if (!res.ok) { | ||||
| 				throw `${res.status} ${res.statusText}`; | ||||
| 			} else { | ||||
| 				return res.json(); | ||||
| 			} | ||||
| 		}); | ||||
| 	return fetch(`http://localhost:${port}/api/drive/files/create`, { | ||||
| 		method: 'post', | ||||
| 		body: formData, | ||||
| 		timeout: 30 * 1000, | ||||
| 	}).then(res => { | ||||
| 		if (!res.ok) { | ||||
| 			throw `${res.status} ${res.statusText}`; | ||||
| 		} else { | ||||
| 			return res.json(); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> { | ||||
|  | @ -94,9 +100,9 @@ export function connectStream(user: any, channel: string, listener: (message: Re | |||
| 		ws.on('open', () => { | ||||
| 			ws.on('message', data => { | ||||
| 				const msg = JSON.parse(data.toString()); | ||||
| 				if (msg.type == 'channel' && msg.body.id == 'a') { | ||||
| 				if (msg.type === 'channel' && msg.body.id === 'a') { | ||||
| 					listener(msg.body); | ||||
| 				} else if (msg.type == 'connected' && msg.body.id == 'a') { | ||||
| 				} else if (msg.type === 'connected' && msg.body.id === 'a') { | ||||
| 					res(ws); | ||||
| 				} | ||||
| 			}); | ||||
|  | @ -107,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re | |||
| 					channel: channel, | ||||
| 					id: 'a', | ||||
| 					pong: true, | ||||
| 					params: params | ||||
| 				} | ||||
| 					params: params, | ||||
| 				}, | ||||
| 			})); | ||||
| 		}); | ||||
| 	}); | ||||
|  | @ -119,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? | |||
| 	return await new Promise((resolve, reject) => { | ||||
| 		const req = http.request(`http://localhost:${port}${path}`, { | ||||
| 			headers: { | ||||
| 				Accept: accept | ||||
| 			} | ||||
| 				Accept: accept, | ||||
| 			}, | ||||
| 		}, res => { | ||||
| 			if (res.statusCode! >= 400) { | ||||
| 				reject(res); | ||||
|  | @ -139,9 +145,9 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? | |||
| 
 | ||||
| export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) { | ||||
| 	return (done: (err?: Error) => any) => { | ||||
| 		const p = childProcess.spawn('node', [__dirname + '/../index.js'], { | ||||
| 		const p = childProcess.spawn('node', [_dirname + '/../index.js'], { | ||||
| 			stdio: ['inherit', 'inherit', 'inherit', 'ipc'], | ||||
| 			env: { NODE_ENV: 'test', PATH: process.env.PATH } | ||||
| 			env: { NODE_ENV: 'test', PATH: process.env.PATH }, | ||||
| 		}); | ||||
| 		callbackSpawnedProcess(p); | ||||
| 		p.on('message', message => { | ||||
|  | @ -153,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce | |||
| export async function initTestDb(justBorrow = false, initEntities?: any[]) { | ||||
| 	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; | ||||
| 
 | ||||
| 	try { | ||||
| 		const conn = await getConnection(); | ||||
| 		await conn.close(); | ||||
| 	} catch (e) {} | ||||
| 
 | ||||
| 	return await createConnection({ | ||||
| 	const db = new DataSource({ | ||||
| 		type: 'postgres', | ||||
| 		host: config.db.host, | ||||
| 		port: config.db.port, | ||||
|  | @ -167,8 +168,12 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { | |||
| 		database: config.db.db, | ||||
| 		synchronize: true && !justBorrow, | ||||
| 		dropSchema: true && !justBorrow, | ||||
| 		entities: initEntities || entities | ||||
| 		entities: initEntities || entities, | ||||
| 	}); | ||||
| 
 | ||||
| 	await db.initialize(); | ||||
| 
 | ||||
| 	return db; | ||||
| } | ||||
| 
 | ||||
| export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> { | ||||
|  | @ -178,9 +183,9 @@ export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProc | |||
| 			rej('timeout to start'); | ||||
| 		}, timeout); | ||||
| 
 | ||||
| 		const p = childProcess.spawn('node', [__dirname + '/../built/index.js'], { | ||||
| 		const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], { | ||||
| 			stdio: ['inherit', 'inherit', 'inherit', 'ipc'], | ||||
| 			env: { NODE_ENV: 'test', PATH: process.env.PATH } | ||||
| 			env: { NODE_ENV: 'test', PATH: process.env.PATH }, | ||||
| 		}); | ||||
| 
 | ||||
| 		p.on('error', e => rej(e)); | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,68 +1,79 @@ | |||
| module.exports = { | ||||
| 	root: true, | ||||
| 	env: { | ||||
| 		"node": false | ||||
| 		'node': false, | ||||
| 	}, | ||||
| 	parser: "vue-eslint-parser", | ||||
| 	parser: 'vue-eslint-parser', | ||||
| 	parserOptions: { | ||||
| 		"parser": "@typescript-eslint/parser", | ||||
| 		'parser': '@typescript-eslint/parser', | ||||
| 		tsconfigRootDir: __dirname, | ||||
| 		//project: ['./tsconfig.json'],
 | ||||
| 		project: ['./tsconfig.json'], | ||||
| 		extraFileExtensions: ['.vue'], | ||||
| 	}, | ||||
| 	extends: [ | ||||
| 		//"../shared/.eslintrc.js",
 | ||||
| 		"plugin:vue/vue3-recommended" | ||||
| 		'../shared/.eslintrc.js', | ||||
| 		'plugin:vue/vue3-recommended', | ||||
| 	], | ||||
| 	rules: { | ||||
| 		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 | ||||
| 		// data の禁止理由: 抽象的すぎるため
 | ||||
| 		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | ||||
| 		"id-denylist": ["error", "window", "data", "e"], | ||||
| 		'id-denylist': ['error', 'window', 'data', 'e'], | ||||
| 		'eqeqeq': ['error', 'always', { 'null': 'ignore' }], | ||||
| 		"no-shadow": ["warn"], | ||||
| 		"vue/attributes-order": ["error", { | ||||
| 			"alphabetical": false | ||||
| 		'no-shadow': ['warn'], | ||||
| 		'vue/attributes-order': ['error', { | ||||
| 			'alphabetical': false, | ||||
| 		}], | ||||
| 		"vue/no-use-v-if-with-v-for": ["error", { | ||||
| 			"allowUsingIterationVar": false | ||||
| 		'vue/no-use-v-if-with-v-for': ['error', { | ||||
| 			'allowUsingIterationVar': false, | ||||
| 		}], | ||||
| 		"vue/no-ref-as-operand": "error", | ||||
| 		"vue/no-multi-spaces": ["error", { | ||||
| 			"ignoreProperties": false | ||||
| 		'vue/no-ref-as-operand': 'error', | ||||
| 		'vue/no-multi-spaces': ['error', { | ||||
| 			'ignoreProperties': false, | ||||
| 		}], | ||||
| 		"vue/no-v-html": "error", | ||||
| 		"vue/order-in-components": "error", | ||||
| 		"vue/html-indent": ["warn", "tab", { | ||||
| 			"attribute": 1, | ||||
| 			"baseIndent": 0, | ||||
| 			"closeBracket": 0, | ||||
| 			"alignAttributesVertically": true, | ||||
| 			"ignores": [] | ||||
| 		'vue/no-v-html': 'error', | ||||
| 		'vue/order-in-components': 'error', | ||||
| 		'vue/html-indent': ['warn', 'tab', { | ||||
| 			'attribute': 1, | ||||
| 			'baseIndent': 0, | ||||
| 			'closeBracket': 0, | ||||
| 			'alignAttributesVertically': true, | ||||
| 			'ignores': [], | ||||
| 		}], | ||||
| 		"vue/html-closing-bracket-spacing": ["warn", { | ||||
| 			"startTag": "never", | ||||
| 			"endTag": "never", | ||||
| 			"selfClosingTag": "never" | ||||
| 		'vue/html-closing-bracket-spacing': ['warn', { | ||||
| 			'startTag': 'never', | ||||
| 			'endTag': 'never', | ||||
| 			'selfClosingTag': 'never', | ||||
| 		}], | ||||
| 		"vue/multi-word-component-names": "warn", | ||||
| 		"vue/require-v-for-key": "warn", | ||||
| 		"vue/no-unused-components": "warn", | ||||
| 		"vue/valid-v-for": "warn", | ||||
| 		"vue/return-in-computed-property": "warn", | ||||
| 		"vue/no-setup-props-destructure": "warn", | ||||
| 		"vue/max-attributes-per-line": "off", | ||||
| 		"vue/html-self-closing": "off", | ||||
| 		"vue/singleline-html-element-content-newline": "off", | ||||
| 		'vue/multi-word-component-names': 'warn', | ||||
| 		'vue/require-v-for-key': 'warn', | ||||
| 		'vue/no-unused-components': 'warn', | ||||
| 		'vue/valid-v-for': 'warn', | ||||
| 		'vue/return-in-computed-property': 'warn', | ||||
| 		'vue/no-setup-props-destructure': 'warn', | ||||
| 		'vue/max-attributes-per-line': 'off', | ||||
| 		'vue/html-self-closing': 'off', | ||||
| 		'vue/singleline-html-element-content-newline': 'off', | ||||
| 	}, | ||||
| 	globals: { | ||||
| 		"require": false, | ||||
| 		"_DEV_": false, | ||||
| 		"_LANGS_": false, | ||||
| 		"_VERSION_": false, | ||||
| 		"_ENV_": false, | ||||
| 		"_PERF_PREFIX_": false, | ||||
| 		"_DATA_TRANSFER_DRIVE_FILE_": false, | ||||
| 		"_DATA_TRANSFER_DRIVE_FOLDER_": false, | ||||
| 		"_DATA_TRANSFER_DECK_COLUMN_": false | ||||
| 	} | ||||
| } | ||||
| 		// Node.js
 | ||||
| 		'module': false, | ||||
| 		'require': false, | ||||
| 		'__dirname': false, | ||||
| 
 | ||||
| 		// Vue
 | ||||
| 		'$$': false, | ||||
| 		'$ref': false, | ||||
| 		'$computed': false, | ||||
| 
 | ||||
| 		// Misskey
 | ||||
| 		'_DEV_': false, | ||||
| 		'_LANGS_': false, | ||||
| 		'_VERSION_': false, | ||||
| 		'_ENV_': false, | ||||
| 		'_PERF_PREFIX_': false, | ||||
| 		'_DATA_TRANSFER_DRIVE_FILE_': false, | ||||
| 		'_DATA_TRANSFER_DRIVE_FOLDER_': false, | ||||
| 		'_DATA_TRANSFER_DECK_COLUMN_': false, | ||||
| 	}, | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										8
									
								
								packages/client/@types/theme.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								packages/client/@types/theme.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,7 @@ | |||
| import { Theme } from '../src/scripts/theme'; | ||||
| 
 | ||||
| declare module '@/themes/*.json5' { | ||||
| 	export = Theme; | ||||
| 	import { Theme } from "@/scripts/theme"; | ||||
| 
 | ||||
| 	const theme: Theme; | ||||
| 
 | ||||
| 	export default theme; | ||||
| } | ||||
|  |  | |||
|  | @ -10,47 +10,37 @@ | |||
| 		"lodash": "^4.17.21" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@discordapp/twemoji": "13.1.1", | ||||
| 		"@discordapp/twemoji": "14.0.2", | ||||
| 		"@fortawesome/fontawesome-free": "6.1.1", | ||||
| 		"@rollup/plugin-alias": "3.1.9", | ||||
| 		"@rollup/plugin-json": "4.1.0", | ||||
| 		"@syuilo/aiscript": "0.11.1", | ||||
| 		"@typescript-eslint/parser": "5.20.0", | ||||
| 		"@vitejs/plugin-vue": "2.3.1", | ||||
| 		"@vue/compiler-sfc": "3.2.33", | ||||
| 		"abort-controller": "3.0.0", | ||||
| 		"autobind-decorator": "2.4.0", | ||||
| 		"autosize": "5.0.1", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"blurhash": "1.1.5", | ||||
| 		"broadcast-channel": "4.11.0", | ||||
| 		"broadcast-channel": "4.12.0", | ||||
| 		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2", | ||||
| 		"chart.js": "3.7.1", | ||||
| 		"chart.js": "3.8.0", | ||||
| 		"chartjs-adapter-date-fns": "2.0.0", | ||||
| 		"chartjs-plugin-gradient": "0.2.2", | ||||
| 		"chartjs-plugin-gradient": "0.5.0", | ||||
| 		"chartjs-plugin-zoom": "1.2.1", | ||||
| 		"compare-versions": "4.1.3", | ||||
| 		"content-disposition": "0.5.4", | ||||
| 		"date-fns": "2.28.0", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "8.14.0", | ||||
| 		"eslint-plugin-vue": "8.7.1", | ||||
| 		"eventemitter3": "4.0.7", | ||||
| 		"feed": "4.2.2", | ||||
| 		"glob": "7.2.0", | ||||
| 		"idb-keyval": "6.1.0", | ||||
| 		"insert-text-at-cursor": "0.3.0", | ||||
| 		"json5": "2.2.1", | ||||
| 		"katex": "0.15.3", | ||||
| 		"katex": "0.15.6", | ||||
| 		"matter-js": "0.18.0", | ||||
| 		"mfm-js": "0.21.0", | ||||
| 		"mfm-js": "0.22.1", | ||||
| 		"misskey-js": "0.0.14", | ||||
| 		"mocha": "9.2.2", | ||||
| 		"mocha": "10.0.0", | ||||
| 		"ms": "2.1.3", | ||||
| 		"nested-property": "4.0.0", | ||||
| 		"parse5": "6.0.1", | ||||
| 		"photoswipe": "5.2.4", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"photoswipe": "5.2.7", | ||||
| 		"prismjs": "1.28.0", | ||||
| 		"private-ip": "2.3.3", | ||||
| 		"promise-limit": "2.7.0", | ||||
|  | @ -61,31 +51,35 @@ | |||
| 		"random-seed": "0.3.0", | ||||
| 		"reflect-metadata": "0.1.13", | ||||
| 		"rndstr": "1.0.0", | ||||
| 		"rollup": "2.70.2", | ||||
| 		"s-age": "1.1.2", | ||||
| 		"sass": "1.50.1", | ||||
| 		"sass": "1.52.1", | ||||
| 		"seedrandom": "3.0.5", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"syuilo-password-strength": "0.0.1", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"three": "0.139.2", | ||||
| 		"throttle-debounce": "4.0.1", | ||||
| 		"three": "0.140.2", | ||||
| 		"throttle-debounce": "5.0.0", | ||||
| 		"tinycolor2": "1.4.2", | ||||
| 		"tsc-alias": "1.5.0", | ||||
| 		"tsconfig-paths": "3.14.1", | ||||
| 		"tsc-alias": "1.6.7", | ||||
| 		"tsconfig-paths": "4.0.0", | ||||
| 		"twemoji-parser": "14.0.0", | ||||
| 		"typescript": "4.6.3", | ||||
| 		"uuid": "8.3.2", | ||||
| 		"v-debounce": "0.1.2", | ||||
| 		"vanilla-tilt": "1.7.2", | ||||
| 		"vite": "2.9.6", | ||||
| 		"vue": "3.2.33", | ||||
| 		"vue": "3.2.36", | ||||
| 		"vue-prism-editor": "2.0.0-alpha.2", | ||||
| 		"vue-router": "4.0.14", | ||||
| 		"vue-router": "4.0.15", | ||||
| 		"vuedraggable": "4.0.1", | ||||
| 		"websocket": "1.0.34", | ||||
| 		"ws": "8.5.0" | ||||
| 		"@vitejs/plugin-vue": "2.3.3", | ||||
| 		"@vue/compiler-sfc": "3.2.36", | ||||
| 		"@rollup/plugin-alias": "3.1.9", | ||||
| 		"@rollup/plugin-json": "4.1.0", | ||||
| 		"rollup": "2.74.1", | ||||
| 		"typescript": "4.7.2", | ||||
| 		"vite": "2.9.9", | ||||
| 		"ws": "8.6.0" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@types/escape-regexp": "0.0.1", | ||||
|  | @ -97,19 +91,21 @@ | |||
| 		"@types/matter-js": "0.17.7", | ||||
| 		"@types/mocha": "9.1.1", | ||||
| 		"@types/oauth": "0.9.1", | ||||
| 		"@types/parse5": "6.0.3", | ||||
| 		"@types/punycode": "2.1.0", | ||||
| 		"@types/qrcode": "1.4.2", | ||||
| 		"@types/random-seed": "0.3.3", | ||||
| 		"@types/seedrandom": "3.0.2", | ||||
| 		"@types/throttle-debounce": "4.0.0", | ||||
| 		"@types/throttle-debounce": "5.0.0", | ||||
| 		"@types/tinycolor2": "1.4.3", | ||||
| 		"@types/uuid": "8.3.4", | ||||
| 		"@types/websocket": "1.0.5", | ||||
| 		"@types/ws": "8.5.3", | ||||
| 		"@typescript-eslint/eslint-plugin": "5.20.0", | ||||
| 		"@typescript-eslint/eslint-plugin": "5.26.0", | ||||
| 		"@typescript-eslint/parser": "5.26.0", | ||||
| 		"eslint": "8.16.0", | ||||
| 		"eslint-plugin-vue": "9.0.1", | ||||
| 		"cross-env": "7.0.3", | ||||
| 		"cypress": "9.5.4", | ||||
| 		"cypress": "9.7.0", | ||||
| 		"eslint-plugin-import": "2.26.0", | ||||
| 		"start-server-and-test": "1.14.0" | ||||
| 	} | ||||
|  |  | |||
|  | @ -11,10 +11,10 @@ import { i18n } from './i18n'; | |||
| 
 | ||||
| type Account = misskey.entities.MeDetailed; | ||||
| 
 | ||||
| const data = localStorage.getItem('account'); | ||||
| const accountData = localStorage.getItem('account'); | ||||
| 
 | ||||
| // TODO: 外部からはreadonlyに
 | ||||
| export const $i = data ? reactive(JSON.parse(data) as Account) : null; | ||||
| export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; | ||||
| 
 | ||||
| export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); | ||||
| 
 | ||||
|  | @ -52,7 +52,7 @@ export async function signout() { | |||
| 					return Promise.all(registrations.map(registration => registration.unregister())); | ||||
| 				}); | ||||
| 		} | ||||
| 	} catch (e) {} | ||||
| 	} catch (err) {} | ||||
| 	//#endregion
 | ||||
| 
 | ||||
| 	document.cookie = `igi=; path=/`; | ||||
|  | @ -104,8 +104,8 @@ function fetchAccount(token: string): Promise<Account> { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function updateAccount(data) { | ||||
| 	for (const [key, value] of Object.entries(data)) { | ||||
| export function updateAccount(accountData) { | ||||
| 	for (const [key, value] of Object.entries(accountData)) { | ||||
| 		$i[key] = value; | ||||
| 	} | ||||
| 	localStorage.setItem('account', JSON.stringify($i)); | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| const window = ref<InstanceType<typeof XWindow>>(); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <div class="bcekxzvu _card _gap"> | ||||
| 	<div class="_content target"> | ||||
| 		<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> | ||||
| 		<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId"> | ||||
| 		<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)"> | ||||
| 			<MkUserName class="name" :user="report.targetUser"/> | ||||
| 			<MkAcct class="acct" :user="report.targetUser" style="display: block;"/> | ||||
| 		</MkA> | ||||
|  |  | |||
|  | @ -48,8 +48,8 @@ async function onClick() { | |||
| 			}); | ||||
| 			isFollowing.value = true; | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		console.error(e); | ||||
| 	} catch (err) { | ||||
| 		console.error(err); | ||||
| 	} finally { | ||||
| 		wait.value = false; | ||||
| 	} | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -18,7 +18,7 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'update:modelValue', v: boolean): void; | ||||
| 	(ev: 'update:modelValue', v: boolean): void; | ||||
| }>(); | ||||
| 
 | ||||
| const label = computed(() => { | ||||
|  |  | |||
|  | @ -90,8 +90,8 @@ const props = withDefaults(defineProps<{ | |||
| }); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'done', v: { canceled: boolean; result: any }): void; | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'done', v: { canceled: boolean; result: any }): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| const modal = ref<InstanceType<typeof MkModal>>(); | ||||
|  | @ -122,14 +122,14 @@ function onBgClick() { | |||
| 	if (props.cancelableByBgClick) cancel(); | ||||
| } | ||||
| */ | ||||
| function onKeydown(e: KeyboardEvent) { | ||||
| 	if (e.key === 'Escape') cancel(); | ||||
| function onKeydown(evt: KeyboardEvent) { | ||||
| 	if (evt.key === 'Escape') cancel(); | ||||
| } | ||||
| 
 | ||||
| function onInputKeydown(e: KeyboardEvent) { | ||||
| 	if (e.key === 'Enter') { | ||||
| 		e.preventDefault(); | ||||
| 		e.stopPropagation(); | ||||
| function onInputKeydown(evt: KeyboardEvent) { | ||||
| 	if (evt.key === 'Enter') { | ||||
| 		evt.preventDefault(); | ||||
| 		evt.stopPropagation(); | ||||
| 		ok(); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -58,7 +58,7 @@ if (props.user.isFollowing == null) { | |||
| } | ||||
| 
 | ||||
| function onFollowChange(user: Misskey.entities.UserDetailed) { | ||||
| 	if (user.id == props.user.id) { | ||||
| 	if (user.id === props.user.id) { | ||||
| 		isFollowing.value = user.isFollowing; | ||||
| 		hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; | ||||
| 	} | ||||
|  | @ -96,8 +96,8 @@ async function onClick() { | |||
| 				hasPendingFollowRequestFromYou.value = true; | ||||
| 			} | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		console.error(e); | ||||
| 	} catch (err) { | ||||
| 		console.error(err); | ||||
| 	} finally { | ||||
| 		wait.value = false; | ||||
| 	} | ||||
|  |  | |||
|  | @ -41,8 +41,8 @@ import { instance } from '@/instance'; | |||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'done'): void; | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'done'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| let dialog: InstanceType<typeof XModalWindow> = $ref(); | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ | |||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormRange> | ||||
| 				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock"> | ||||
| 				<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)"> | ||||
| 					<span v-text="form[item].content || item"></span> | ||||
| 				</MkButton> | ||||
| 			</template> | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'update:modelValue', v: boolean): void; | ||||
| 	(ev: 'update:modelValue', v: boolean): void; | ||||
| }>(); | ||||
| 
 | ||||
| let button = $ref<HTMLElement>(); | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ const props = withDefaults(defineProps<{ | |||
| }); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'click', ev: MouseEvent): void; | ||||
| 	(ev: 'click', v: MouseEvent): void; | ||||
| }>(); | ||||
| 
 | ||||
| const url = $computed(() => defaultStore.state.disableShowingAnimatedImages | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ export default defineComponent({ | |||
| 		const url = computed(() => { | ||||
| 			if (char.value) { | ||||
| 				let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16)); | ||||
| 				if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); | ||||
| 				if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); | ||||
| 				codes = codes.filter(x => x && x.length); | ||||
| 				return `${twemojiSvgBase}/${codes.join('-')}.svg`; | ||||
| 			} else { | ||||
|  |  | |||
|  | @ -1,11 +1,24 @@ | |||
| <template> | ||||
| <div class="yxspomdl" :class="{ inline, colored, mini }"> | ||||
| 	<div class="ring"></div> | ||||
| <div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini }]"> | ||||
| 	<div :class="$style.container"> | ||||
| 		<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> | ||||
| 			<g transform="matrix(1.125,0,0,1.125,12,12)"> | ||||
| 				<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> | ||||
| 			</g> | ||||
| 		</svg> | ||||
| 		<svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> | ||||
| 			<g transform="matrix(1.125,0,0,1.125,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:21.33px;"/> | ||||
| 			</g> | ||||
| 		</svg> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import { useCssModule } from 'vue'; | ||||
| 
 | ||||
| useCssModule(); | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	inline?: boolean; | ||||
|  | @ -18,8 +31,8 @@ const props = withDefaults(defineProps<{ | |||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| @keyframes ring { | ||||
| <style lang="scss" module> | ||||
| @keyframes spinner { | ||||
| 	0% { | ||||
| 		transform: rotate(0deg); | ||||
| 	} | ||||
|  | @ -28,12 +41,12 @@ const props = withDefaults(defineProps<{ | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .yxspomdl { | ||||
| .root { | ||||
| 	padding: 32px; | ||||
| 	text-align: center; | ||||
| 	cursor: wait; | ||||
| 
 | ||||
| 	--size: 48px; | ||||
| 	--size: 40px; | ||||
| 
 | ||||
| 	&.colored { | ||||
| 		color: var(--accent); | ||||
|  | @ -49,34 +62,33 @@ const props = withDefaults(defineProps<{ | |||
| 		padding: 16px; | ||||
| 		--size: 32px; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 	> .ring { | ||||
| 		position: relative; | ||||
| 		display: inline-block; | ||||
| 		vertical-align: middle; | ||||
| .container { | ||||
| 	position: relative; | ||||
| 	width: var(--size); | ||||
| 	height: var(--size); | ||||
| 	margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| 		&:before, | ||||
| 		&:after { | ||||
| 			content: " "; | ||||
| 			display: block; | ||||
| 			box-sizing: border-box; | ||||
| 			width: var(--size); | ||||
| 			height: var(--size); | ||||
| 			border-radius: 50%; | ||||
| 			border: solid 4px; | ||||
| 		} | ||||
| .spinner { | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	width: var(--size); | ||||
| 	height: var(--size); | ||||
| 	fill-rule: evenodd; | ||||
| 	clip-rule: evenodd; | ||||
| 	stroke-linecap: round; | ||||
| 	stroke-linejoin: round; | ||||
| 	stroke-miterlimit: 1.5; | ||||
| } | ||||
| 
 | ||||
| 		&:before { | ||||
| 			border-color: currentColor; | ||||
| 			opacity: 0.3; | ||||
| 		} | ||||
| .bg { | ||||
| 	opacity: 0.275; | ||||
| } | ||||
| 
 | ||||
| 		&:after { | ||||
| 			position: absolute; | ||||
| 			top: 0; | ||||
| 			border-color: currentColor transparent transparent transparent; | ||||
| 			animation: ring 0.5s linear infinite; | ||||
| 		} | ||||
| 	} | ||||
| .fg { | ||||
| 	animation: spinner 0.5s linear infinite; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -31,6 +31,32 @@ const props = withDefaults(defineProps<{ | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .mfm-x2 { | ||||
| 	--mfm-zoom-size: 200%; | ||||
| } | ||||
| 
 | ||||
| .mfm-x3 { | ||||
| 	--mfm-zoom-size: 400%; | ||||
| } | ||||
| 
 | ||||
| .mfm-x4 { | ||||
| 	--mfm-zoom-size: 600%; | ||||
| } | ||||
| 
 | ||||
| .mfm-x2, .mfm-x3, .mfm-x4 { | ||||
| 	font-size: var(--mfm-zoom-size); | ||||
| 
 | ||||
| 	.mfm-x2, .mfm-x3, .mfm-x4 { | ||||
| 		/* only half effective */ | ||||
| 		font-size: calc(var(--mfm-zoom-size) / 2 + 50%); | ||||
| 
 | ||||
| 		.mfm-x2, .mfm-x3, .mfm-x4 { | ||||
| 			/* disabled */ | ||||
| 			font-size: 100%; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @keyframes mfm-spin { | ||||
| 	0% { transform: rotate(0deg); } | ||||
| 	100% { transform: rotate(360deg); } | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ const props = withDefaults(defineProps<{ | |||
| 	mode: 'relative', | ||||
| }); | ||||
| 
 | ||||
| const _time = typeof props.time == 'string' ? new Date(props.time) : props.time; | ||||
| const _time = typeof props.time === 'string' ? new Date(props.time) : props.time; | ||||
| const absolute = _time.toLocaleString(); | ||||
| 
 | ||||
| let now = $ref(new Date()); | ||||
|  | @ -32,8 +32,7 @@ const relative = $computed(() => { | |||
| 		ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : | ||||
| 		ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : | ||||
| 		ago >= -1       ? i18n.ts._ago.justNow : | ||||
| 		ago <  -1       ? i18n.ts._ago.future : | ||||
| 		i18n.ts._ago.unknown); | ||||
| 		i18n.ts._ago.future); | ||||
| }); | ||||
| 
 | ||||
| function tick() { | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ const props = withDefaults(defineProps<{ | |||
| }); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| const modal = $ref<InstanceType<typeof MkModal>>(); | ||||
|  |  | |||
|  | @ -39,6 +39,19 @@ const bg = { | |||
| 	border-radius: 4px 0 0 4px; | ||||
| 	overflow: hidden; | ||||
| 	color: #fff; | ||||
| 	text-shadow: /* .866 ≈ sin(60deg) */ | ||||
| 		1px 0 1px #000, | ||||
| 		.866px .5px 1px #000, | ||||
| 		.5px .866px 1px #000, | ||||
| 		0 1px 1px #000, | ||||
| 		-.5px .866px 1px #000, | ||||
| 		-.866px .5px 1px #000, | ||||
| 		-1px 0 1px #000, | ||||
| 		-.866px -.5px 1px #000, | ||||
| 		-.5px -.866px 1px #000, | ||||
| 		0 -1px 1px #000, | ||||
| 		.5px -.866px 1px #000, | ||||
| 		.866px -.5px 1px #000; | ||||
| 
 | ||||
| 	> .icon { | ||||
| 		height: 100%; | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 	computed: { | ||||
| 		remainingLength(): number { | ||||
| 			if (typeof this.inputValue != "string") return 512; | ||||
| 			if (typeof this.inputValue !== "string") return 512; | ||||
| 			return 512 - length(this.inputValue); | ||||
| 		} | ||||
| 	}, | ||||
|  | @ -116,17 +116,17 @@ export default defineComponent({ | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onKeydown(e) { | ||||
| 			if (e.which === 27) { // ESC | ||||
| 		onKeydown(evt) { | ||||
| 			if (evt.which === 27) { // ESC | ||||
| 				this.cancel(); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onInputKeydown(e) { | ||||
| 			if (e.which === 13) { // Enter | ||||
| 				if (e.ctrlKey) { | ||||
| 					e.preventDefault(); | ||||
| 					e.stopPropagation(); | ||||
| 		onInputKeydown(evt) { | ||||
| 			if (evt.which === 13) { // Enter | ||||
| 				if (evt.ctrlKey) { | ||||
| 					evt.preventDefault(); | ||||
| 					evt.stopPropagation(); | ||||
| 					this.ok(); | ||||
| 				} | ||||
| 			} | ||||
|  |  | |||
|  | @ -142,16 +142,19 @@ export default defineComponent({ | |||
| 							break; | ||||
| 						} | ||||
| 						case 'x2': { | ||||
| 							style = `font-size: 200%;`; | ||||
| 							break; | ||||
| 							return h('span', { | ||||
| 								class: 'mfm-x2', | ||||
| 							}, genEl(token.children)); | ||||
| 						} | ||||
| 						case 'x3': { | ||||
| 							style = `font-size: 400%;`; | ||||
| 							break; | ||||
| 							return h('span', { | ||||
| 								class: 'mfm-x3', | ||||
| 							}, genEl(token.children)); | ||||
| 						} | ||||
| 						case 'x4': { | ||||
| 							style = `font-size: 600%;`; | ||||
| 							break; | ||||
| 							return h('span', { | ||||
| 								class: 'mfm-x4', | ||||
| 							}, genEl(token.children)); | ||||
| 						} | ||||
| 						case 'font': { | ||||
| 							const family = | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ | |||
| <div | ||||
| 	v-if="!muted" | ||||
| 	v-show="!isDeleted" | ||||
| 	ref="el" | ||||
| 	v-hotkey="keymap" | ||||
| 	v-size="{ max: [500, 450, 350, 300] }" | ||||
| 	ref="el" | ||||
| 	class="lxwezrsl _block" | ||||
| 	:tabindex="!isDeleted ? '-1' : null" | ||||
| 	:class="{ renote: isRenote }" | ||||
|  | @ -197,7 +197,7 @@ const keymap = { | |||
| 	'q': () => renoteButton.value.renote(true), | ||||
| 	'esc': blur, | ||||
| 	'm|o': () => menu(true), | ||||
| 	's': () => showContent.value != showContent.value, | ||||
| 	's': () => showContent.value !== showContent.value, | ||||
| }; | ||||
| 
 | ||||
| useNoteCapture({ | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ const keymap = { | |||
| 	'down|j|tab': focusAfter, | ||||
| 	'esc': blur, | ||||
| 	'm|o': () => menu(true), | ||||
| 	's': () => showContent.value != showContent.value, | ||||
| 	's': () => showContent.value !== showContent.value, | ||||
| }; | ||||
| 
 | ||||
| useNoteCapture({ | ||||
|  |  | |||
|  | @ -1,34 +1,22 @@ | |||
| <template> | ||||
| <div class="lzyxtsnt"> | ||||
| 	<img v-if="image" :src="image.url"/> | ||||
| 	<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import { defineComponent, PropType } from 'vue'; | ||||
| import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { ImageBlock } from '@/scripts/hpml/block'; | ||||
| import { Hpml } from '@/scripts/hpml/evaluator'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		block: { | ||||
| 			type: Object as PropType<ImageBlock>, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		hpml: { | ||||
| 			type: Object as PropType<Hpml>, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 	setup(props, ctx) { | ||||
| 		const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId); | ||||
| const props = defineProps<{ | ||||
| 	block: PropType<ImageBlock>, | ||||
| 	hpml: PropType<Hpml>, | ||||
| }>(); | ||||
| 
 | ||||
| 		return { | ||||
| 			image | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -52,16 +52,16 @@ export default defineComponent({ | |||
| 			const promise = new Promise((ok) => { | ||||
| 				const canvas = this.hpml.canvases[this.block.canvasId]; | ||||
| 				canvas.toBlob(blob => { | ||||
| 					const data = new FormData(); | ||||
| 					data.append('file', blob); | ||||
| 					data.append('i', this.$i.token); | ||||
| 					const formData = new FormData(); | ||||
| 					formData.append('file', blob); | ||||
| 					formData.append('i', this.$i.token); | ||||
| 					if (this.$store.state.uploadFolder) { | ||||
| 						data.append('folderId', this.$store.state.uploadFolder); | ||||
| 						formData.append('folderId', this.$store.state.uploadFolder); | ||||
| 					} | ||||
| 
 | ||||
| 					fetch(apiUrl + '/drive/files/create', { | ||||
| 						method: 'POST', | ||||
| 						body: data | ||||
| 						body: formData, | ||||
| 					}) | ||||
| 					.then(response => response.json()) | ||||
| 					.then(f => { | ||||
|  |  | |||
|  | @ -38,8 +38,8 @@ export default defineComponent({ | |||
| 					let ast; | ||||
| 					try { | ||||
| 						ast = parse(props.page.script); | ||||
| 					} catch (e) { | ||||
| 						console.error(e); | ||||
| 					} catch (err) { | ||||
| 						console.error(err); | ||||
| 						/*os.alert({ | ||||
| 							type: 'error', | ||||
| 							text: 'Syntax error :(' | ||||
|  | @ -48,11 +48,11 @@ export default defineComponent({ | |||
| 					} | ||||
| 					hpml.aiscript.exec(ast).then(() => { | ||||
| 						hpml.eval(); | ||||
| 					}).catch(e => { | ||||
| 						console.error(e); | ||||
| 					}).catch(err => { | ||||
| 						console.error(err); | ||||
| 						/*os.alert({ | ||||
| 							type: 'error', | ||||
| 							text: e | ||||
| 							text: err | ||||
| 						});*/ | ||||
| 					}); | ||||
| 				} else { | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ function add() { | |||
| } | ||||
| 
 | ||||
| function remove(i) { | ||||
| 	choices.value = choices.value.filter((_, _i) => _i != i); | ||||
| 	choices.value = choices.value.filter((_, _i) => _i !== i); | ||||
| } | ||||
| 
 | ||||
| function get() { | ||||
|  |  | |||
|  | @ -98,7 +98,7 @@ export default defineComponent({ | |||
| 			}, { | ||||
| 				done: result => { | ||||
| 					if (!result || result.canceled) return; | ||||
| 					let comment = result.result.length == 0 ? null : result.result; | ||||
| 					let comment = result.result.length === 0 ? null : result.result; | ||||
| 					os.api('drive/files/update', { | ||||
| 						fileId: file.id, | ||||
| 						comment: comment, | ||||
|  |  | |||
|  | @ -107,7 +107,7 @@ const props = withDefaults(defineProps<{ | |||
| 	fixed?: boolean; | ||||
| 	autofocus?: boolean; | ||||
| }>(), { | ||||
| 	initialVisibleUsers: [], | ||||
| 	initialVisibleUsers: () => [], | ||||
| 	autofocus: true, | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ | |||
| 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||
| 	@click="toggleReaction()" | ||||
| > | ||||
| 	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/> | ||||
| 	<span>{{ count }}</span> | ||||
| 	<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/> | ||||
| 	<span class="count">{{ count }}</span> | ||||
| </button> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -141,12 +141,16 @@ export default defineComponent({ | |||
| 			background: var(--accent); | ||||
| 		} | ||||
| 
 | ||||
| 		> span { | ||||
| 		> .count { | ||||
| 			color: var(--fgOnAccent); | ||||
| 		} | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> span { | ||||
| 	> .count { | ||||
| 		font-size: 0.9em; | ||||
| 		line-height: 32px; | ||||
| 		margin: 0 0 0 4px; | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue