Create Mika
This commit is contained in:
		
							parent
							
								
									6683d50bae
								
							
						
					
					
						commit
						9d328784ec
					
				
					 5 changed files with 382 additions and 16 deletions
				
			
		| 
						 | 
				
			
			@ -68,6 +68,7 @@ src ... ソースコード
 | 
			
		|||
	server ... Webサーバー
 | 
			
		||||
	client ... クライアント
 | 
			
		||||
	mfm ... MFM
 | 
			
		||||
	sanctuary ... TypeScriptの制約を強くしたエリア ~~乃々の聖域ではない~~
 | 
			
		||||
 | 
			
		||||
test ... テスト
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,9 @@ import * as fs from 'fs';
 | 
			
		|||
import { URL } from 'url';
 | 
			
		||||
import * as yaml from 'js-yaml';
 | 
			
		||||
import { Source, Mixin } from './types';
 | 
			
		||||
import Mika, { optional } from '../sanctuary/mika';
 | 
			
		||||
import * as pkg from '../../package.json';
 | 
			
		||||
import Logger from '../misc/logger';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Path of configuration directory
 | 
			
		||||
| 
						 | 
				
			
			@ -21,27 +23,138 @@ const path = process.env.NODE_ENV == 'test'
 | 
			
		|||
	: `${dir}/default.yml`;
 | 
			
		||||
 | 
			
		||||
export default function load() {
 | 
			
		||||
	const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
 | 
			
		||||
	const config: Source = yaml.safeLoad(fs.readFileSync(path, 'utf-8'));
 | 
			
		||||
 | 
			
		||||
	const mixin = {} as Mixin;
 | 
			
		||||
	const logger = new Logger('config');
 | 
			
		||||
 | 
			
		||||
	const errors = new Mika({
 | 
			
		||||
		repository_url: 'string!?',
 | 
			
		||||
		feedback_url: 'string!?',
 | 
			
		||||
		url: 'string!',
 | 
			
		||||
		port: 'number!',
 | 
			
		||||
		https: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			key: 'string!',
 | 
			
		||||
			cert: 'string!'
 | 
			
		||||
		},
 | 
			
		||||
		disableHsts: 'boolean!?',
 | 
			
		||||
		mongodb: {
 | 
			
		||||
			host: 'string!',
 | 
			
		||||
			port: 'number!',
 | 
			
		||||
			db: 'string!',
 | 
			
		||||
			user: 'string!?',
 | 
			
		||||
			pass: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		elasticsearch: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			host: 'string!',
 | 
			
		||||
			port: 'number!',
 | 
			
		||||
			pass: 'string!'
 | 
			
		||||
		},
 | 
			
		||||
		drive: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			storage: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		redis: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			host: 'string!',
 | 
			
		||||
			port: 'number!',
 | 
			
		||||
			pass: 'string!'
 | 
			
		||||
		},
 | 
			
		||||
		autoAdmin: 'boolean!?',
 | 
			
		||||
		proxy: 'string!?',
 | 
			
		||||
		accesslog: 'string!?',
 | 
			
		||||
		clusterLimit: 'number!?',
 | 
			
		||||
		// The below properties are defined for backward compatibility.
 | 
			
		||||
		name: 'string!?',
 | 
			
		||||
		description: 'string!?',
 | 
			
		||||
		localDriveCapacityMb: 'number!?',
 | 
			
		||||
		remoteDriveCapacityMb: 'number!?',
 | 
			
		||||
		preventCacheRemoteFiles: 'boolean!?',
 | 
			
		||||
		recaptcha: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			enableRecaptcha: 'boolean!?',
 | 
			
		||||
			recaptchaSiteKey: 'string!?',
 | 
			
		||||
			recaptchaSecretKey: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		ghost: 'string!?',
 | 
			
		||||
		maintainer: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			name: 'string!',
 | 
			
		||||
			email: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		twitter: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			consumer_key: 'string!?',
 | 
			
		||||
			consumer_secret: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		github: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			client_id: 'string!?',
 | 
			
		||||
			client_secret: 'string!?'
 | 
			
		||||
		},
 | 
			
		||||
		user_recommendation: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			engine: 'string!?',
 | 
			
		||||
			timeout: 'number!?'
 | 
			
		||||
		},
 | 
			
		||||
		sw: {
 | 
			
		||||
			[optional]: true,
 | 
			
		||||
			public_key: 'string!',
 | 
			
		||||
			private_key: 'string!'
 | 
			
		||||
		}
 | 
			
		||||
	}).validate(config);
 | 
			
		||||
 | 
			
		||||
	if (!errors && config.drive.storage === 'minio') {
 | 
			
		||||
		const minioErrors = new Mika({
 | 
			
		||||
			bucket: 'string!',
 | 
			
		||||
			prefix: 'string!',
 | 
			
		||||
			baseUrl: 'string!',
 | 
			
		||||
			config: {
 | 
			
		||||
				endPoint: 'string!',
 | 
			
		||||
				accessKey: 'string!',
 | 
			
		||||
				secretKey: 'string!',
 | 
			
		||||
				useSSL: 'boolean!?',
 | 
			
		||||
				port: 'number!?',
 | 
			
		||||
				region: 'string!?',
 | 
			
		||||
				transport: 'string!?',
 | 
			
		||||
				sessionToken: 'string!?',
 | 
			
		||||
			}
 | 
			
		||||
		}).validate(config.drive);
 | 
			
		||||
 | 
			
		||||
		if (minioErrors)
 | 
			
		||||
			for (const error of minioErrors)
 | 
			
		||||
				errors.push(error);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (errors) {
 | 
			
		||||
		for (const { path, excepted, actual } of errors)
 | 
			
		||||
			logger.error(`Invalid config value detected at ${path}: excepted type is ${typeof excepted === 'string' ? excepted : 'object'}, but actual value type is ${actual}`);
 | 
			
		||||
 | 
			
		||||
		throw 'The configuration is invalid. Check your .config/default.yml';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const url = validateUrl(config.url);
 | 
			
		||||
 | 
			
		||||
	config.url = normalizeUrl(config.url);
 | 
			
		||||
 | 
			
		||||
	mixin.host = url.host;
 | 
			
		||||
	mixin.hostname = url.hostname;
 | 
			
		||||
	mixin.scheme = url.protocol.replace(/:$/, '');
 | 
			
		||||
	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
 | 
			
		||||
	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
 | 
			
		||||
	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
 | 
			
		||||
	mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
 | 
			
		||||
	mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
 | 
			
		||||
	mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
 | 
			
		||||
	mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
 | 
			
		||||
	mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
 | 
			
		||||
	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
 | 
			
		||||
	mixin.user_agent = `Misskey/${pkg.version} (${config.url})`;
 | 
			
		||||
	const scheme = url.protocol.replace(/:$/, '');
 | 
			
		||||
	const ws_scheme = scheme.replace('http', 'ws');
 | 
			
		||||
 | 
			
		||||
	const mixin: Mixin = {
 | 
			
		||||
		host: url.host,
 | 
			
		||||
		hostname: url.hostname,
 | 
			
		||||
		scheme,
 | 
			
		||||
		ws_scheme,
 | 
			
		||||
		ws_url: `${ws_scheme}://${url.host}`,
 | 
			
		||||
		api_url: `${scheme}://${url.host}/api`,
 | 
			
		||||
		auth_url: `${scheme}://${url.host}/auth`,
 | 
			
		||||
		dev_url: `${scheme}://${url.host}/dev`,
 | 
			
		||||
		docs_url: `${scheme}://${url.host}/docs`,
 | 
			
		||||
		stats_url: `${scheme}://${url.host}/stats`,
 | 
			
		||||
		status_url: `${scheme}://${url.host}/status`,
 | 
			
		||||
		drive_url: `${scheme}://${url.host}/files`,
 | 
			
		||||
		user_agent: `Misskey/${pkg.version} (${config.url})`
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	if (config.autoAdmin == null) config.autoAdmin = false;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,3 +115,18 @@ export function cumulativeSum(xs: number[]): number[] {
 | 
			
		|||
	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
 | 
			
		||||
	return ys;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toEnglishString(x: string[], n = 'and'): string {
 | 
			
		||||
	switch (x.length) {
 | 
			
		||||
		case 0:
 | 
			
		||||
			return '';
 | 
			
		||||
 | 
			
		||||
		case 1:
 | 
			
		||||
			return x[0];
 | 
			
		||||
 | 
			
		||||
		default:
 | 
			
		||||
			const y = [...x];
 | 
			
		||||
			const z = y.pop();
 | 
			
		||||
			return `${y.join(', ')}, ${n} ${z}`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										230
									
								
								src/sanctuary/mika.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								src/sanctuary/mika.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,230 @@
 | 
			
		|||
/* tslint:enable:strict-type-predicates triple-equals */
 | 
			
		||||
 | 
			
		||||
import { toEnglishString } from '../prelude/array';
 | 
			
		||||
 | 
			
		||||
/* KEYWORD DEFINITION
 | 
			
		||||
 * { sakura: null } // 'sakura' is null.
 | 
			
		||||
 * { izumi: undefined } // 'izumi' is undefined.
 | 
			
		||||
 * {} // 'ako' is unprovided (not undefined in here).
 | 
			
		||||
 *
 | 
			
		||||
 * Reason: The undefined is a type, so you can define undefined.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const additional = Symbol('Allows additional properties.');
 | 
			
		||||
export const optional = Symbol('Allows unprovided (not undefined).');
 | 
			
		||||
export const nullable = Symbol('Allows null.');
 | 
			
		||||
 | 
			
		||||
type Type = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
 | 
			
		||||
type ExtendedType = Type | 'null' | 'unprovided';
 | 
			
		||||
 | 
			
		||||
type Everything = string | number | bigint | boolean | symbol | undefined | object | Function | null;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manifest
 | 
			
		||||
 * Information for
 | 
			
		||||
 * Reliance
 | 
			
		||||
 * Identify
 | 
			
		||||
 * Analysis
 | 
			
		||||
 */
 | 
			
		||||
type Miria = {
 | 
			
		||||
	/** STRING TYPED SYNTAX
 | 
			
		||||
	 * type1       : Allows the type1 and null.
 | 
			
		||||
	 * type1|type2 : Allows type1, type2, and null.
 | 
			
		||||
	 * type1!      : Allows type1 only.
 | 
			
		||||
	 * type1?      : Allows type1, null, and unprovided.
 | 
			
		||||
	 * type1!?     : Allows type1, and unprovided.
 | 
			
		||||
	 *
 | 
			
		||||
	 * (! and ? are suffix.)
 | 
			
		||||
	 * (The spaces(U+0020) are ignored.)
 | 
			
		||||
	 */
 | 
			
		||||
	[x: string]: string | Miria;
 | 
			
		||||
	[additional]?: boolean;
 | 
			
		||||
	[optional]?: boolean;
 | 
			
		||||
	[nullable]?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Reason
 | 
			
		||||
 * Information for
 | 
			
		||||
 * Keeping safety by
 | 
			
		||||
 * Analysis
 | 
			
		||||
 */
 | 
			
		||||
type Rika = {
 | 
			
		||||
	path: string;
 | 
			
		||||
	excepted: string | Miria | null;
 | 
			
		||||
	actual: ExtendedType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type MiriaInternal = {
 | 
			
		||||
	[x: string]: string | MiriaInternal | undefined;
 | 
			
		||||
	[additional]?: boolean;
 | 
			
		||||
	[optional]?: boolean;
 | 
			
		||||
	[nullable]?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $ = () => {}; // trash
 | 
			
		||||
 | 
			
		||||
// ^ https://github.com/Microsoft/TypeScript/issues/7061
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manifest based
 | 
			
		||||
 * Identify objects for
 | 
			
		||||
 * Keeping safety by this
 | 
			
		||||
 * Analyzer class
 | 
			
		||||
 */
 | 
			
		||||
export default class Mika {
 | 
			
		||||
	private static readonly fuhihi = 'Miria'; // < https://github.com/Microsoft/TypeScript/issues/1579
 | 
			
		||||
 | 
			
		||||
	protected static readonly types = ['string', 'number', 'bigint', 'boolean', 'symbol', 'undefined', 'object', 'function'];
 | 
			
		||||
 | 
			
		||||
	protected static readonly syntax = new RegExp(`^\\s*(?:${Mika.types.join('|')})(?:\\s*\\|\\s*(?:${Mika.types.join('|')}))*\\s*!?\\s*\\??\\s*$`);
 | 
			
		||||
 | 
			
		||||
	public static readonly additional = additional;
 | 
			
		||||
 | 
			
		||||
	public static readonly optional = optional;
 | 
			
		||||
 | 
			
		||||
	public static readonly nullable = nullable;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		readonly miria: Miria
 | 
			
		||||
	) {
 | 
			
		||||
		Mika.ensure(miria, [], Object.keys({ miria })[0]);
 | 
			
		||||
		//                     ^~~ #1579 (see above) ~~^
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static ensure(
 | 
			
		||||
		source: Miria,
 | 
			
		||||
		location: string[],
 | 
			
		||||
		nameof: string
 | 
			
		||||
	) {
 | 
			
		||||
		const errorMessage = `Specified ${nameof} is invalid.`;
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(source)) {
 | 
			
		||||
			const invalidType = !['string', 'object'].includes(typeof v);
 | 
			
		||||
			const header = () => `${errorMessage} ${[...location, k].join('.')} is`;
 | 
			
		||||
 | 
			
		||||
			if (invalidType || v === null)
 | 
			
		||||
				throw `${header()} ${invalidType ? `${typeof v === 'undefined' ? 'an' : 'a'} ${typeof v}, neither string or object(: ${Mika.fuhihi})` : 'is null'}.`;
 | 
			
		||||
 | 
			
		||||
			if (typeof v === 'string' && !Mika.syntax.test(v))
 | 
			
		||||
				throw `${header()} '${v}', neither ${toEnglishString([...Mika.types, 'combined theirs'], 'or')}.`;
 | 
			
		||||
 | 
			
		||||
			if (typeof v === 'object')
 | 
			
		||||
				this.ensure(v, [...location, k], errorMessage);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected static validateFromString(
 | 
			
		||||
		target: Everything,
 | 
			
		||||
		source: string,
 | 
			
		||||
		location: string[] = []
 | 
			
		||||
	) {
 | 
			
		||||
		const path = location.join('.');
 | 
			
		||||
		const rikas: Rika[] = [];
 | 
			
		||||
		const excepted = source.replace(/\s/, '');
 | 
			
		||||
		/*let it move! v
 | 
			
		||||
		*/let lastSpan = excepted.length - 1;
 | 
			
		||||
		const optional = excepted[lastSpan] === '?';
 | 
			
		||||
		const nullable = excepted[optional ? --lastSpan : lastSpan] !== '!';
 | 
			
		||||
		const allowing = excepted.slice(0, nullable ? --lastSpan : lastSpan).split('|');
 | 
			
		||||
		const pushRika = (actual: ExtendedType) => rikas.push({ path, excepted, actual });
 | 
			
		||||
 | 
			
		||||
		if (target === null)
 | 
			
		||||
			(nullable || pushRika('null'), $)();
 | 
			
		||||
		else if (!allowing.includes(typeof target))
 | 
			
		||||
			pushRika(typeof target);
 | 
			
		||||
 | 
			
		||||
		return rikas;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected dive(
 | 
			
		||||
		target: object,
 | 
			
		||||
		source: MiriaInternal,
 | 
			
		||||
		location: string[] = []
 | 
			
		||||
	) {
 | 
			
		||||
		const rikas: Rika[] = [];
 | 
			
		||||
		/** DEFINITION
 | 
			
		||||
		 * |   values   | specs | given |
 | 
			
		||||
		 * |-----------:|:------|:------|
 | 
			
		||||
		 * |       true | true  | true  |
 | 
			
		||||
		 * |      false | false | true  |
 | 
			
		||||
		 * |       null | true  | false |
 | 
			
		||||
		 * | unprovided | false | false |
 | 
			
		||||
		 */
 | 
			
		||||
		const keys = Object.keys(source).reduce<Record<string, boolean | null>>((a, c) => (a[c] = null, a), {});
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(target) as [string, Everything][]) {
 | 
			
		||||
			const inclusion = keys[k] !== undefined;
 | 
			
		||||
			const x = source[k];
 | 
			
		||||
			const miria = x as Miria;
 | 
			
		||||
			const here = [...location, k];
 | 
			
		||||
			const path = here.join('.');
 | 
			
		||||
			const pushRika = (actual: ExtendedType, excepted?: string | Miria | null) => rikas.push({
 | 
			
		||||
				path,
 | 
			
		||||
				excepted: excepted === undefined ? miria : excepted,
 | 
			
		||||
				actual
 | 
			
		||||
			});
 | 
			
		||||
			const pushRikas = (iterable: Iterable<Rika>) => {
 | 
			
		||||
				for (const rika of iterable)
 | 
			
		||||
					rikas.push(rika);
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			keys[k] = inclusion;
 | 
			
		||||
 | 
			
		||||
			if (!inclusion && !source[additional])
 | 
			
		||||
				pushRika(v === null ? 'null' : typeof v, null);
 | 
			
		||||
			else if (typeof x === 'undefined')
 | 
			
		||||
				continue;
 | 
			
		||||
			else if (typeof x === 'string')
 | 
			
		||||
				pushRikas(Mika.validateFromString(v, x, here));
 | 
			
		||||
			else if (v === undefined)
 | 
			
		||||
				(x[optional] || pushRika(inclusion ? 'undefined' : 'unprovided'), $)();
 | 
			
		||||
			else if (v === null)
 | 
			
		||||
				(x[nullable] || pushRika('null'), $)();
 | 
			
		||||
			else if (typeof v === 'object')
 | 
			
		||||
				pushRikas(this.dive(v, x, here));
 | 
			
		||||
			else
 | 
			
		||||
				pushRika(typeof v);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(source as Miria).filter(([k]) => keys[k] === null)) {
 | 
			
		||||
			const rika: Rika = {
 | 
			
		||||
				path: [...location, k].join('.'),
 | 
			
		||||
				excepted: v,
 | 
			
		||||
				actual: 'unprovided'
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			if (typeof v === 'string')
 | 
			
		||||
				(v.endsWith('?') || rikas.push(rika), $)();
 | 
			
		||||
			else if (!v[optional])
 | 
			
		||||
				rikas.push(rika);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return rikas;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Validates object.
 | 
			
		||||
	 * @param x The source object.
 | 
			
		||||
	 * @returns The difference points when the source object fails validation, otherwise null.
 | 
			
		||||
	 */
 | 
			
		||||
	public validate(
 | 
			
		||||
		x: object
 | 
			
		||||
	) {
 | 
			
		||||
		const root = (actual: ExtendedType): Rika[] => [{
 | 
			
		||||
			path: '',
 | 
			
		||||
			excepted: this.miria,
 | 
			
		||||
			actual
 | 
			
		||||
		}];
 | 
			
		||||
 | 
			
		||||
		if (typeof x !== 'object')
 | 
			
		||||
			return root(typeof x);
 | 
			
		||||
 | 
			
		||||
		if (x === null)
 | 
			
		||||
			return this.miria[nullable] ? null : root('null');
 | 
			
		||||
 | 
			
		||||
		const rikas = this.dive(x, this.miria);
 | 
			
		||||
 | 
			
		||||
		return rikas.length ? rikas : null;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/sanctuary/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/sanctuary/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
	"extends": "../../tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
		"target": "esnext",
 | 
			
		||||
		"strictNullChecks": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue