Fastify (#9106)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * Update SignupApiService.ts * wip * wip * Update ClientServerService.ts * wip * wip * wip * Update WellKnownServerService.ts * wip * wip * update des * wip * Update ApiServerService.ts * wip * update deps * Update WellKnownServerService.ts * wip * update deps * Update ApiCallService.ts * Update ApiCallService.ts * Update ApiServerService.ts
This commit is contained in:
		
							parent
							
								
									2db9f6efe7
								
							
						
					
					
						commit
						3a7182bfb5
					
				
					 40 changed files with 1651 additions and 1977 deletions
				
			
		| 
						 | 
					@ -21,20 +21,19 @@
 | 
				
			||||||
		"@tensorflow/tfjs-node": "4.1.0"
 | 
							"@tensorflow/tfjs-node": "4.1.0"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
		"@bull-board/api": "4.3.1",
 | 
					 | 
				
			||||||
		"@bull-board/koa": "4.3.1",
 | 
					 | 
				
			||||||
		"@bull-board/ui": "4.3.1",
 | 
					 | 
				
			||||||
		"@discordapp/twemoji": "14.0.2",
 | 
							"@discordapp/twemoji": "14.0.2",
 | 
				
			||||||
		"@elastic/elasticsearch": "7.17.0",
 | 
							"@fastify/accepts": "4.0.1",
 | 
				
			||||||
		"@koa/cors": "3.3.0",
 | 
							"@fastify/cors": "8.2.0",
 | 
				
			||||||
		"@koa/multer": "3.0.0",
 | 
							"@fastify/multipart": "7.3.0",
 | 
				
			||||||
		"@koa/router": "9.0.1",
 | 
							"@fastify/static": "6.5.0",
 | 
				
			||||||
 | 
							"@fastify/view": "7.1.2",
 | 
				
			||||||
		"@nestjs/common": "9.2.0",
 | 
							"@nestjs/common": "9.2.0",
 | 
				
			||||||
		"@nestjs/core": "9.2.0",
 | 
							"@nestjs/core": "9.2.0",
 | 
				
			||||||
		"@nestjs/testing": "9.2.0",
 | 
							"@nestjs/testing": "9.2.0",
 | 
				
			||||||
		"@peertube/http-signature": "1.7.0",
 | 
							"@peertube/http-signature": "1.7.0",
 | 
				
			||||||
		"@sinonjs/fake-timers": "10.0.0",
 | 
							"@sinonjs/fake-timers": "10.0.0",
 | 
				
			||||||
		"@syuilo/aiscript": "0.11.1",
 | 
							"@syuilo/aiscript": "0.11.1",
 | 
				
			||||||
 | 
							"accepts": "^1.3.8",
 | 
				
			||||||
		"ajv": "8.11.2",
 | 
							"ajv": "8.11.2",
 | 
				
			||||||
		"archiver": "5.3.1",
 | 
							"archiver": "5.3.1",
 | 
				
			||||||
		"autobind-decorator": "2.4.0",
 | 
							"autobind-decorator": "2.4.0",
 | 
				
			||||||
| 
						 | 
					@ -54,6 +53,7 @@
 | 
				
			||||||
		"date-fns": "2.29.3",
 | 
							"date-fns": "2.29.3",
 | 
				
			||||||
		"deep-email-validator": "0.1.21",
 | 
							"deep-email-validator": "0.1.21",
 | 
				
			||||||
		"escape-regexp": "0.0.1",
 | 
							"escape-regexp": "0.0.1",
 | 
				
			||||||
 | 
							"fastify": "4.10.0",
 | 
				
			||||||
		"feed": "4.2.2",
 | 
							"feed": "4.2.2",
 | 
				
			||||||
		"file-type": "18.0.0",
 | 
							"file-type": "18.0.0",
 | 
				
			||||||
		"fluent-ffmpeg": "2.1.2",
 | 
							"fluent-ffmpeg": "2.1.2",
 | 
				
			||||||
| 
						 | 
					@ -69,20 +69,10 @@
 | 
				
			||||||
		"json5-loader": "4.0.1",
 | 
							"json5-loader": "4.0.1",
 | 
				
			||||||
		"jsonld": "8.1.0",
 | 
							"jsonld": "8.1.0",
 | 
				
			||||||
		"jsrsasign": "10.6.1",
 | 
							"jsrsasign": "10.6.1",
 | 
				
			||||||
		"koa": "2.13.4",
 | 
					 | 
				
			||||||
		"koa-bodyparser": "4.3.0",
 | 
					 | 
				
			||||||
		"koa-favicon": "2.1.0",
 | 
					 | 
				
			||||||
		"koa-json-body": "5.3.0",
 | 
					 | 
				
			||||||
		"koa-logger": "3.2.1",
 | 
					 | 
				
			||||||
		"koa-mount": "4.0.0",
 | 
					 | 
				
			||||||
		"koa-send": "5.0.1",
 | 
					 | 
				
			||||||
		"koa-slow": "2.1.0",
 | 
					 | 
				
			||||||
		"koa-views": "7.0.2",
 | 
					 | 
				
			||||||
		"mfm-js": "0.23.0",
 | 
							"mfm-js": "0.23.0",
 | 
				
			||||||
		"mime-types": "2.1.35",
 | 
							"mime-types": "2.1.35",
 | 
				
			||||||
		"misskey-js": "0.0.14",
 | 
							"misskey-js": "0.0.14",
 | 
				
			||||||
		"ms": "3.0.0-canary.1",
 | 
							"ms": "3.0.0-canary.1",
 | 
				
			||||||
		"multer": "1.4.4",
 | 
					 | 
				
			||||||
		"nested-property": "4.0.0",
 | 
							"nested-property": "4.0.0",
 | 
				
			||||||
		"node-fetch": "3.3.0",
 | 
							"node-fetch": "3.3.0",
 | 
				
			||||||
		"nodemailer": "6.8.0",
 | 
							"nodemailer": "6.8.0",
 | 
				
			||||||
| 
						 | 
					@ -129,6 +119,7 @@
 | 
				
			||||||
		"ulid": "2.3.0",
 | 
							"ulid": "2.3.0",
 | 
				
			||||||
		"unzipper": "0.10.11",
 | 
							"unzipper": "0.10.11",
 | 
				
			||||||
		"uuid": "9.0.0",
 | 
							"uuid": "9.0.0",
 | 
				
			||||||
 | 
							"vary": "1.1.2",
 | 
				
			||||||
		"web-push": "3.5.0",
 | 
							"web-push": "3.5.0",
 | 
				
			||||||
		"websocket": "1.0.34",
 | 
							"websocket": "1.0.34",
 | 
				
			||||||
		"ws": "8.11.0",
 | 
							"ws": "8.11.0",
 | 
				
			||||||
| 
						 | 
					@ -138,6 +129,7 @@
 | 
				
			||||||
		"@redocly/openapi-core": "1.0.0-beta.114",
 | 
							"@redocly/openapi-core": "1.0.0-beta.114",
 | 
				
			||||||
		"@swc/core": "1.3.20",
 | 
							"@swc/core": "1.3.20",
 | 
				
			||||||
		"@swc/jest": "0.2.23",
 | 
							"@swc/jest": "0.2.23",
 | 
				
			||||||
 | 
							"@types/accepts": "1.3.5",
 | 
				
			||||||
		"@types/archiver": "5.3.1",
 | 
							"@types/archiver": "5.3.1",
 | 
				
			||||||
		"@types/bcryptjs": "2.4.2",
 | 
							"@types/bcryptjs": "2.4.2",
 | 
				
			||||||
		"@types/bull": "4.10.0",
 | 
							"@types/bull": "4.10.0",
 | 
				
			||||||
| 
						 | 
					@ -149,17 +141,6 @@
 | 
				
			||||||
		"@types/jsdom": "20.0.1",
 | 
							"@types/jsdom": "20.0.1",
 | 
				
			||||||
		"@types/jsonld": "1.5.8",
 | 
							"@types/jsonld": "1.5.8",
 | 
				
			||||||
		"@types/jsrsasign": "10.5.4",
 | 
							"@types/jsrsasign": "10.5.4",
 | 
				
			||||||
		"@types/koa": "2.13.5",
 | 
					 | 
				
			||||||
		"@types/koa-bodyparser": "4.3.8",
 | 
					 | 
				
			||||||
		"@types/koa-cors": "0.0.2",
 | 
					 | 
				
			||||||
		"@types/koa-favicon": "2.0.21",
 | 
					 | 
				
			||||||
		"@types/koa-logger": "3.1.2",
 | 
					 | 
				
			||||||
		"@types/koa-mount": "4.0.1",
 | 
					 | 
				
			||||||
		"@types/koa-send": "4.1.3",
 | 
					 | 
				
			||||||
		"@types/koa-views": "7.0.0",
 | 
					 | 
				
			||||||
		"@types/koa__cors": "3.3.0",
 | 
					 | 
				
			||||||
		"@types/koa__multer": "2.0.4",
 | 
					 | 
				
			||||||
		"@types/koa__router": "8.0.11",
 | 
					 | 
				
			||||||
		"@types/mime-types": "2.1.1",
 | 
							"@types/mime-types": "2.1.1",
 | 
				
			||||||
		"@types/node": "18.11.9",
 | 
							"@types/node": "18.11.9",
 | 
				
			||||||
		"@types/node-fetch": "3.0.3",
 | 
							"@types/node-fetch": "3.0.3",
 | 
				
			||||||
| 
						 | 
					@ -182,6 +163,7 @@
 | 
				
			||||||
		"@types/tmp": "0.2.3",
 | 
							"@types/tmp": "0.2.3",
 | 
				
			||||||
		"@types/unzipper": "0.10.5",
 | 
							"@types/unzipper": "0.10.5",
 | 
				
			||||||
		"@types/uuid": "8.3.4",
 | 
							"@types/uuid": "8.3.4",
 | 
				
			||||||
 | 
							"@types/vary": "1.1.0",
 | 
				
			||||||
		"@types/web-push": "3.3.2",
 | 
							"@types/web-push": "3.3.2",
 | 
				
			||||||
		"@types/websocket": "1.0.5",
 | 
							"@types/websocket": "1.0.5",
 | 
				
			||||||
		"@types/ws": "8.5.3",
 | 
							"@types/ws": "8.5.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								packages/backend/src/@types/koa-json-body.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								packages/backend/src/@types/koa-json-body.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,15 +0,0 @@
 | 
				
			||||||
declare module 'koa-json-body' {
 | 
					 | 
				
			||||||
	import type { Middleware } from 'koa';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	interface IKoaJsonBodyOptions {
 | 
					 | 
				
			||||||
		strict: boolean;
 | 
					 | 
				
			||||||
		limit: string;
 | 
					 | 
				
			||||||
		fallback: boolean;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function koaJsonBody(opt?: IKoaJsonBodyOptions): Middleware;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	namespace koaJsonBody {} // Hack
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export = koaJsonBody;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										14
									
								
								packages/backend/src/@types/koa-slow.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								packages/backend/src/@types/koa-slow.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,14 +0,0 @@
 | 
				
			||||||
declare module 'koa-slow' {
 | 
					 | 
				
			||||||
	import type { Middleware } from 'koa';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	interface ISlowOptions {
 | 
					 | 
				
			||||||
		url?: RegExp;
 | 
					 | 
				
			||||||
		delay?: number;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function slow(options?: ISlowOptions): Middleware;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	namespace slow {} // Hack
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export = slow;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -52,6 +52,7 @@ if (!envOption.quiet) {
 | 
				
			||||||
process.on('uncaughtException', err => {
 | 
					process.on('uncaughtException', err => {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		logger.error(err);
 | 
							logger.error(err);
 | 
				
			||||||
 | 
							console.trace(err);
 | 
				
			||||||
	} catch { }
 | 
						} catch { }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,9 +45,13 @@ export class CaptchaService {
 | 
				
			||||||
		return await res.json() as CaptchaResponse;
 | 
							return await res.json() as CaptchaResponse;
 | 
				
			||||||
	}	
 | 
						}	
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public async verifyRecaptcha(secret: string, response: string): Promise<void> {
 | 
						public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
 | 
				
			||||||
		const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
 | 
							if (response == null) {
 | 
				
			||||||
			throw `recaptcha-request-failed: ${e}`;
 | 
								throw 'recaptcha-failed: no response provided';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
 | 
				
			||||||
 | 
								throw `recaptcha-request-failed: ${err}`;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (result.success !== true) {
 | 
							if (result.success !== true) {
 | 
				
			||||||
| 
						 | 
					@ -56,9 +60,13 @@ export class CaptchaService {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async verifyHcaptcha(secret: string, response: string): Promise<void> {
 | 
						public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
 | 
				
			||||||
		const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
 | 
							if (response == null) {
 | 
				
			||||||
			throw `hcaptcha-request-failed: ${e}`;
 | 
								throw 'hcaptcha-failed: no response provided';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
 | 
				
			||||||
 | 
								throw `hcaptcha-request-failed: ${err}`;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (result.success !== true) {
 | 
							if (result.success !== true) {
 | 
				
			||||||
| 
						 | 
					@ -67,9 +75,13 @@ export class CaptchaService {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async verifyTurnstile(secret: string, response: string): Promise<void> {
 | 
						public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
 | 
				
			||||||
		const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(e => {
 | 
							if (response == null) {
 | 
				
			||||||
			throw `turnstile-request-failed: ${e}`;
 | 
								throw 'turnstile-failed: no response provided';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
 | 
				
			||||||
 | 
								throw `turnstile-request-failed: ${err}`;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (result.success !== true) {
 | 
							if (result.success !== true) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -674,7 +674,7 @@ export class ApRendererService {
 | 
				
			||||||
	 * @param last URL of last page (optional)
 | 
						 * @param last URL of last page (optional)
 | 
				
			||||||
	 * @param orderedItems attached objects (optional)
 | 
						 * @param orderedItems attached objects (optional)
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: Record<string, unknown>[]) {
 | 
						public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) {
 | 
				
			||||||
		const page: any = {
 | 
							const page: any = {
 | 
				
			||||||
			id,
 | 
								id,
 | 
				
			||||||
			type: 'OrderedCollection',
 | 
								type: 'OrderedCollection',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,6 @@ const envOption = {
 | 
				
			||||||
	verbose: false,
 | 
						verbose: false,
 | 
				
			||||||
	withLogTime: false,
 | 
						withLogTime: false,
 | 
				
			||||||
	quiet: false,
 | 
						quiet: false,
 | 
				
			||||||
	slow: false,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
 | 
					for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ export function createTempDir(): Promise<[string, () => void]> {
 | 
				
			||||||
			(e, path, cleanup) => {
 | 
								(e, path, cleanup) => {
 | 
				
			||||||
				if (e) return rej(e);
 | 
									if (e) return rej(e);
 | 
				
			||||||
				res([path, cleanup]);
 | 
									res([path, cleanup]);
 | 
				
			||||||
			}
 | 
								},
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								packages/backend/src/misc/fastify-reply-error.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/backend/src/misc/fastify-reply-error.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					// https://www.fastify.io/docs/latest/Reference/Reply/#async-await-and-promises
 | 
				
			||||||
 | 
					export class FastifyReplyError extends Error {
 | 
				
			||||||
 | 
						public message: string;
 | 
				
			||||||
 | 
						public statusCode: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(statusCode: number, message: string) {
 | 
				
			||||||
 | 
							super(message);
 | 
				
			||||||
 | 
							this.message = message;
 | 
				
			||||||
 | 
							this.statusCode = statusCode;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,9 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import json from 'koa-json-body';
 | 
					import fastifyAccepts from '@fastify/accepts';
 | 
				
			||||||
import httpSignature from '@peertube/http-signature';
 | 
					import httpSignature from '@peertube/http-signature';
 | 
				
			||||||
import { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
 | 
					import { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
 | 
				
			||||||
 | 
					import accepts from 'accepts';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import * as url from '@/misc/prelude/url.js';
 | 
					import * as url from '@/misc/prelude/url.js';
 | 
				
			||||||
| 
						 | 
					@ -56,14 +57,15 @@ export class ActivityPubServerService {
 | 
				
			||||||
		private userKeypairStoreService: UserKeypairStoreService,
 | 
							private userKeypairStoreService: UserKeypairStoreService,
 | 
				
			||||||
		private queryService: QueryService,
 | 
							private queryService: QueryService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private setResponseType(ctx: Router.RouterContext) {
 | 
						private setResponseType(request: FastifyRequest, reply: FastifyReply): void {
 | 
				
			||||||
		const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
 | 
							const accept = request.accepts().type([ACTIVITY_JSON, LD_JSON]);
 | 
				
			||||||
		if (accept === LD_JSON) {
 | 
							if (accept === LD_JSON) {
 | 
				
			||||||
			ctx.response.type = LD_JSON;
 | 
								reply.type(LD_JSON);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ctx.response.type = ACTIVITY_JSON;
 | 
								reply.type(ACTIVITY_JSON);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -80,31 +82,34 @@ export class ActivityPubServerService {
 | 
				
			||||||
		return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
 | 
							return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private inbox(ctx: Router.RouterContext) {
 | 
						private inbox(request: FastifyRequest, reply: FastifyReply) {
 | 
				
			||||||
		let signature;
 | 
							let signature;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
 | 
								signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			ctx.status = 401;
 | 
								reply.code(401);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.queueService.inbox(ctx.request.body, signature);
 | 
							this.queueService.inbox(request.body, signature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.status = 202;
 | 
							reply.code(202);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async followers(ctx: Router.RouterContext) {
 | 
						private async followers(
 | 
				
			||||||
		const userId = ctx.params.user;
 | 
							request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const userId = request.params.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const cursor = ctx.request.query.cursor;
 | 
							const cursor = request.query.cursor;
 | 
				
			||||||
		if (cursor != null && typeof cursor !== 'string') {
 | 
							if (cursor != null && typeof cursor !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const page = ctx.request.query.page === 'true';
 | 
							const page = request.query.page === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const user = await this.usersRepository.findOneBy({
 | 
							const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
			id: userId,
 | 
								id: userId,
 | 
				
			||||||
| 
						 | 
					@ -112,7 +117,7 @@ export class ActivityPubServerService {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,12 +125,12 @@ export class ActivityPubServerService {
 | 
				
			||||||
		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
							const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (profile.ffVisibility === 'private') {
 | 
							if (profile.ffVisibility === 'private') {
 | 
				
			||||||
			ctx.status = 403;
 | 
								reply.code(403);
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=30');
 | 
								reply.header('Cache-Control', 'public, max-age=30');
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		} else if (profile.ffVisibility === 'followers') {
 | 
							} else if (profile.ffVisibility === 'followers') {
 | 
				
			||||||
			ctx.status = 403;
 | 
								reply.code(403);
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=30');
 | 
								reply.header('Cache-Control', 'public, max-age=30');
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
| 
						 | 
					@ -168,27 +173,30 @@ export class ActivityPubServerService {
 | 
				
			||||||
				})}` : undefined,
 | 
									})}` : undefined,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// index page
 | 
								// index page
 | 
				
			||||||
			const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
 | 
								const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async following(ctx: Router.RouterContext) {
 | 
						private async following(
 | 
				
			||||||
		const userId = ctx.params.user;
 | 
							request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const userId = request.params.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const cursor = ctx.request.query.cursor;
 | 
							const cursor = request.query.cursor;
 | 
				
			||||||
		if (cursor != null && typeof cursor !== 'string') {
 | 
							if (cursor != null && typeof cursor !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const page = ctx.request.query.page === 'true';
 | 
							const page = request.query.page === 'true';
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const user = await this.usersRepository.findOneBy({
 | 
							const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
			id: userId,
 | 
								id: userId,
 | 
				
			||||||
| 
						 | 
					@ -196,7 +204,7 @@ export class ActivityPubServerService {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -204,12 +212,12 @@ export class ActivityPubServerService {
 | 
				
			||||||
		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
							const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (profile.ffVisibility === 'private') {
 | 
							if (profile.ffVisibility === 'private') {
 | 
				
			||||||
			ctx.status = 403;
 | 
								reply.code(403);
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=30');
 | 
								reply.header('Cache-Control', 'public, max-age=30');
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		} else if (profile.ffVisibility === 'followers') {
 | 
							} else if (profile.ffVisibility === 'followers') {
 | 
				
			||||||
			ctx.status = 403;
 | 
								reply.code(403);
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=30');
 | 
								reply.header('Cache-Control', 'public, max-age=30');
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
| 
						 | 
					@ -252,19 +260,19 @@ export class ActivityPubServerService {
 | 
				
			||||||
				})}` : undefined,
 | 
									})}` : undefined,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// index page
 | 
								// index page
 | 
				
			||||||
			const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
 | 
								const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async featured(ctx: Router.RouterContext) {
 | 
						private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
 | 
				
			||||||
		const userId = ctx.params.user;
 | 
							const userId = request.params.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const user = await this.usersRepository.findOneBy({
 | 
							const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
			id: userId,
 | 
								id: userId,
 | 
				
			||||||
| 
						 | 
					@ -272,7 +280,7 @@ export class ActivityPubServerService {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,30 +299,36 @@ export class ActivityPubServerService {
 | 
				
			||||||
			renderedNotes.length, undefined, undefined, renderedNotes,
 | 
								renderedNotes.length, undefined, undefined, renderedNotes,
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.body = this.apRendererService.renderActivity(rendered);
 | 
							reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
		ctx.set('Cache-Control', 'public, max-age=180');
 | 
							this.setResponseType(request, reply);
 | 
				
			||||||
		this.setResponseType(ctx);
 | 
							return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async outbox(ctx: Router.RouterContext) {
 | 
						private async outbox(
 | 
				
			||||||
		const userId = ctx.params.user;
 | 
							request: FastifyRequest<{
 | 
				
			||||||
 | 
								Params: { user: string; };
 | 
				
			||||||
 | 
								Querystring: { since_id?: string; until_id?: string; page?: string; };
 | 
				
			||||||
 | 
							}>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const userId = request.params.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const sinceId = ctx.request.query.since_id;
 | 
							const sinceId = request.query.since_id;
 | 
				
			||||||
		if (sinceId != null && typeof sinceId !== 'string') {
 | 
							if (sinceId != null && typeof sinceId !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const untilId = ctx.request.query.until_id;
 | 
							const untilId = request.query.until_id;
 | 
				
			||||||
		if (untilId != null && typeof untilId !== 'string') {
 | 
							if (untilId != null && typeof untilId !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const page = ctx.request.query.page === 'true';
 | 
							const page = request.query.page === 'true';
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (countIf(x => x != null, [sinceId, untilId]) > 1) {
 | 
							if (countIf(x => x != null, [sinceId, untilId]) > 1) {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -324,7 +338,7 @@ export class ActivityPubServerService {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -362,110 +376,130 @@ export class ActivityPubServerService {
 | 
				
			||||||
				})}` : undefined,
 | 
									})}` : undefined,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// index page
 | 
								// index page
 | 
				
			||||||
			const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
 | 
								const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
 | 
				
			||||||
				`${partOf}?page=true`,
 | 
									`${partOf}?page=true`,
 | 
				
			||||||
				`${partOf}?page=true&since_id=000000000000000000000000`,
 | 
									`${partOf}?page=true&since_id=000000000000000000000000`,
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(rendered);
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(rendered));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async userInfo(ctx: Router.RouterContext, user: User | null) {
 | 
						private async userInfo(request: FastifyRequest, reply: FastifyReply, user: User | null) {
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser));
 | 
							reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
		ctx.set('Cache-Control', 'public, max-age=180');
 | 
							this.setResponseType(request, reply);
 | 
				
			||||||
		this.setResponseType(ctx);
 | 
							return (this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser)));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createRouter() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		// Init router
 | 
							fastify.addConstraintStrategy({
 | 
				
			||||||
		const router = new Router();
 | 
								name: 'apOrHtml',
 | 
				
			||||||
 | 
								storage() {
 | 
				
			||||||
 | 
									const store = {};
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										get(key) {
 | 
				
			||||||
 | 
											return store[key] ?? null;
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										set(key, value) {
 | 
				
			||||||
 | 
											store[key] = value;
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								deriveConstraint(request, ctx) {
 | 
				
			||||||
 | 
									const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
 | 
				
			||||||
 | 
									const isAp = typeof accepted === 'string' && !accepted.match(/html/);
 | 
				
			||||||
 | 
									return isAp ? 'ap' : 'html';
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fastify.register(fastifyAccepts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region Routing
 | 
							//#region Routing
 | 
				
			||||||
		function isActivityPubReq(ctx: Router.RouterContext) {
 | 
					 | 
				
			||||||
			ctx.response.vary('Accept');
 | 
					 | 
				
			||||||
			const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
 | 
					 | 
				
			||||||
			return typeof accepted === 'string' && !accepted.match(/html/);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// inbox
 | 
							// inbox
 | 
				
			||||||
		router.post('/inbox', json(), ctx => this.inbox(ctx));
 | 
							fastify.post('/inbox', async (request, reply) => await this.inbox(request, reply));
 | 
				
			||||||
		router.post('/users/:user/inbox', json(), ctx => this.inbox(ctx));
 | 
							fastify.post('/users/:user/inbox', async (request, reply) => await this.inbox(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// note
 | 
							// note
 | 
				
			||||||
		router.get('/notes/:note', async (ctx, next) => {
 | 
							fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
 | 
				
			||||||
			if (!isActivityPubReq(ctx)) return await next();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const note = await this.notesRepository.findOneBy({
 | 
								const note = await this.notesRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.note,
 | 
									id: request.params.note,
 | 
				
			||||||
				visibility: In(['public' as const, 'home' as const]),
 | 
									visibility: In(['public' as const, 'home' as const]),
 | 
				
			||||||
				localOnly: false,
 | 
									localOnly: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (note == null) {
 | 
								if (note == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// リモートだったらリダイレクト
 | 
								// リモートだったらリダイレクト
 | 
				
			||||||
			if (note.userHost != null) {
 | 
								if (note.userHost != null) {
 | 
				
			||||||
				if (note.uri == null || this.utilityService.isSelfHost(note.userHost)) {
 | 
									if (note.uri == null || this.utilityService.isSelfHost(note.userHost)) {
 | 
				
			||||||
					ctx.status = 500;
 | 
										reply.code(500);
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				ctx.redirect(note.uri);
 | 
									reply.redirect(note.uri);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false));
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false)));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// note activity
 | 
							// note activity
 | 
				
			||||||
		router.get('/notes/:note/activity', async ctx => {
 | 
							fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
 | 
				
			||||||
			const note = await this.notesRepository.findOneBy({
 | 
								const note = await this.notesRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.note,
 | 
									id: request.params.note,
 | 
				
			||||||
				userHost: IsNull(),
 | 
									userHost: IsNull(),
 | 
				
			||||||
				visibility: In(['public' as const, 'home' as const]),
 | 
									visibility: In(['public' as const, 'home' as const]),
 | 
				
			||||||
				localOnly: false,
 | 
									localOnly: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (note == null) {
 | 
								if (note == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(await this.packActivity(note));
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(await this.packActivity(note)));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// outbox
 | 
							// outbox
 | 
				
			||||||
		router.get('/users/:user/outbox', (ctx) => this.outbox(ctx));
 | 
							fastify.get<{
 | 
				
			||||||
 | 
								Params: { user: string; };
 | 
				
			||||||
 | 
								Querystring: { since_id?: string; until_id?: string; page?: string; };
 | 
				
			||||||
 | 
							}>('/users/:user/outbox', async (request, reply) => await this.outbox(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// followers
 | 
							// followers
 | 
				
			||||||
		router.get('/users/:user/followers', (ctx) => this.followers(ctx));
 | 
							fastify.get<{
 | 
				
			||||||
 | 
								Params: { user: string; };
 | 
				
			||||||
 | 
								Querystring: { cursor?: string; page?: string; };
 | 
				
			||||||
 | 
							}>('/users/:user/followers', async (request, reply) => await this.followers(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// following
 | 
							// following
 | 
				
			||||||
		router.get('/users/:user/following', (ctx) => this.following(ctx));
 | 
							fastify.get<{
 | 
				
			||||||
 | 
								Params: { user: string; };
 | 
				
			||||||
 | 
								Querystring: { cursor?: string; page?: string; };
 | 
				
			||||||
 | 
							}>('/users/:user/following', async (request, reply) => await this.following(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// featured
 | 
							// featured
 | 
				
			||||||
		router.get('/users/:user/collections/featured', (ctx) => this.featured(ctx));
 | 
							fastify.get<{ Params: { user: string; }; }>('/users/:user/collections/featured', async (request, reply) => await this.featured(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// publickey
 | 
							// publickey
 | 
				
			||||||
		router.get('/users/:user/publickey', async ctx => {
 | 
							fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => {
 | 
				
			||||||
			const userId = ctx.params.user;
 | 
								const userId = request.params.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				id: userId,
 | 
									id: userId,
 | 
				
			||||||
| 
						 | 
					@ -473,25 +507,23 @@ export class ActivityPubServerService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (user == null) {
 | 
								if (user == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
 | 
								const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.userEntityService.isLocalUser(user)) {
 | 
								if (this.userEntityService.isLocalUser(user)) {
 | 
				
			||||||
				ctx.body = this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair));
 | 
									reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=180');
 | 
									this.setResponseType(request, reply);
 | 
				
			||||||
				this.setResponseType(ctx);
 | 
									return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/users/:user', async (ctx, next) => {
 | 
							fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
 | 
				
			||||||
			if (!isActivityPubReq(ctx)) return await next();
 | 
								const userId = request.params.user;
 | 
				
			||||||
 | 
					 | 
				
			||||||
			const userId = ctx.params.user;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				id: userId,
 | 
									id: userId,
 | 
				
			||||||
| 
						 | 
					@ -499,86 +531,84 @@ export class ActivityPubServerService {
 | 
				
			||||||
				isSuspended: false,
 | 
									isSuspended: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await this.userInfo(ctx, user);
 | 
								return await this.userInfo(request, reply, user);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/@:user', async (ctx, next) => {
 | 
							fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
 | 
				
			||||||
			if (!isActivityPubReq(ctx)) return await next();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				usernameLower: ctx.params.user.toLowerCase(),
 | 
									usernameLower: request.params.user.toLowerCase(),
 | 
				
			||||||
				host: IsNull(),
 | 
									host: IsNull(),
 | 
				
			||||||
				isSuspended: false,
 | 
									isSuspended: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await this.userInfo(ctx, user);
 | 
								return await this.userInfo(request, reply, user);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// emoji
 | 
							// emoji
 | 
				
			||||||
		router.get('/emojis/:emoji', async ctx => {
 | 
							fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => {
 | 
				
			||||||
			const emoji = await this.emojisRepository.findOneBy({
 | 
								const emoji = await this.emojisRepository.findOneBy({
 | 
				
			||||||
				host: IsNull(),
 | 
									host: IsNull(),
 | 
				
			||||||
				name: ctx.params.emoji,
 | 
									name: request.params.emoji,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (emoji == null) {
 | 
								if (emoji == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji));
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji)));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// like
 | 
							// like
 | 
				
			||||||
		router.get('/likes/:like', async ctx => {
 | 
							fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => {
 | 
				
			||||||
			const reaction = await this.noteReactionsRepository.findOneBy({ id: ctx.params.like });
 | 
								const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (reaction == null) {
 | 
								if (reaction == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const note = await this.notesRepository.findOneBy({ id: reaction.noteId });
 | 
								const note = await this.notesRepository.findOneBy({ id: reaction.noteId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (note == null) {
 | 
								if (note == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note));
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note)));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// follow
 | 
							// follow
 | 
				
			||||||
		router.get('/follows/:follower/:followee', async ctx => {
 | 
							fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => {
 | 
				
			||||||
			// This may be used before the follow is completed, so we do not
 | 
								// This may be used before the follow is completed, so we do not
 | 
				
			||||||
			// check if the following exists.
 | 
								// check if the following exists.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const [follower, followee] = await Promise.all([
 | 
								const [follower, followee] = await Promise.all([
 | 
				
			||||||
				this.usersRepository.findOneBy({
 | 
									this.usersRepository.findOneBy({
 | 
				
			||||||
					id: ctx.params.follower,
 | 
										id: request.params.follower,
 | 
				
			||||||
					host: IsNull(),
 | 
										host: IsNull(),
 | 
				
			||||||
				}),
 | 
									}),
 | 
				
			||||||
				this.usersRepository.findOneBy({
 | 
									this.usersRepository.findOneBy({
 | 
				
			||||||
					id: ctx.params.followee,
 | 
										id: request.params.followee,
 | 
				
			||||||
					host: Not(IsNull()),
 | 
										host: Not(IsNull()),
 | 
				
			||||||
				}),
 | 
									}),
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (follower == null || followee == null) {
 | 
								if (follower == null || followee == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee));
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
								this.setResponseType(request, reply);
 | 
				
			||||||
			this.setResponseType(ctx);
 | 
								return (this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return router;
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,8 @@ import * as fs from 'node:fs';
 | 
				
			||||||
import { fileURLToPath } from 'node:url';
 | 
					import { fileURLToPath } from 'node:url';
 | 
				
			||||||
import { dirname } from 'node:path';
 | 
					import { dirname } from 'node:path';
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Koa from 'koa';
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import cors from '@koa/cors';
 | 
					import fastifyStatic from '@fastify/static';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import send from 'koa-send';
 | 
					 | 
				
			||||||
import rename from 'rename';
 | 
					import rename from 'rename';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import type { DriveFilesRepository } from '@/models/index.js';
 | 
					import type { DriveFilesRepository } from '@/models/index.js';
 | 
				
			||||||
| 
						 | 
					@ -46,45 +44,44 @@ export class FileServerService {
 | 
				
			||||||
		private loggerService: LoggerService,
 | 
							private loggerService: LoggerService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		this.logger = this.loggerService.getLogger('server', 'gray', false);
 | 
							this.logger = this.loggerService.getLogger('server', 'gray', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public commonReadableHandlerGenerator(ctx: Koa.Context) {
 | 
						public commonReadableHandlerGenerator(reply: FastifyReply) {
 | 
				
			||||||
		return (e: Error): void => {
 | 
							return (err: Error): void => {
 | 
				
			||||||
			this.logger.error(e);
 | 
								this.logger.error(err);
 | 
				
			||||||
			ctx.status = 500;
 | 
								reply.code(500);
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=300');
 | 
								reply.header('Cache-Control', 'max-age=300');
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public createServer() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const app = new Koa();
 | 
							fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
		app.use(cors());
 | 
								reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
 | 
				
			||||||
		app.use(async (ctx, next) => {
 | 
								done();
 | 
				
			||||||
			ctx.set('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init router
 | 
							fastify.register(fastifyStatic, {
 | 
				
			||||||
		const router = new Router();
 | 
								root: _dirname,
 | 
				
			||||||
 | 
								serve: false,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/app-default.jpg', ctx => {
 | 
							fastify.get('/app-default.jpg', (request, reply) => {
 | 
				
			||||||
			const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
 | 
								const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
 | 
				
			||||||
			ctx.body = file;
 | 
								reply.header('Content-Type', 'image/jpeg');
 | 
				
			||||||
			ctx.set('Content-Type', 'image/jpeg');
 | 
								reply.header('Cache-Control', 'max-age=31536000, immutable');
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=31536000, immutable');
 | 
								return reply.send(file);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/:key', ctx => this.sendDriveFile(ctx));
 | 
							fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply));
 | 
				
			||||||
		router.get('/:key/(.*)', ctx => this.sendDriveFile(ctx));
 | 
							fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Register router
 | 
							done();
 | 
				
			||||||
		app.use(router.routes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return app;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async sendDriveFile(ctx: Koa.Context) {
 | 
						private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
 | 
				
			||||||
		const key = ctx.params.key;
 | 
							const key = request.params.key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Fetch drive file
 | 
							// Fetch drive file
 | 
				
			||||||
		const file = await this.driveFilesRepository.createQueryBuilder('file')
 | 
							const file = await this.driveFilesRepository.createQueryBuilder('file')
 | 
				
			||||||
| 
						 | 
					@ -94,10 +91,9 @@ export class FileServerService {
 | 
				
			||||||
			.getOne();
 | 
								.getOne();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (file == null) {
 | 
							if (file == null) {
 | 
				
			||||||
			ctx.status = 404;
 | 
								reply.code(404);
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=86400');
 | 
								reply.header('Cache-Control', 'max-age=86400');
 | 
				
			||||||
			await send(ctx as any, '/dummy.png', { root: assets });
 | 
								return reply.sendFile('/dummy.png', assets);
 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const isThumbnail = file.thumbnailAccessKey === key;
 | 
							const isThumbnail = file.thumbnailAccessKey === key;
 | 
				
			||||||
| 
						 | 
					@ -135,18 +131,18 @@ export class FileServerService {
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const image = await convertFile();
 | 
										const image = await convertFile();
 | 
				
			||||||
					ctx.body = image.data;
 | 
										reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
 | 
				
			||||||
					ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
 | 
										reply.header('Cache-Control', 'max-age=31536000, immutable');
 | 
				
			||||||
					ctx.set('Cache-Control', 'max-age=31536000, immutable');
 | 
										return image.data;
 | 
				
			||||||
				} catch (err) {
 | 
									} catch (err) {
 | 
				
			||||||
					this.logger.error(`${err}`);
 | 
										this.logger.error(`${err}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					if (err instanceof StatusError && err.isClientError) {
 | 
										if (err instanceof StatusError && err.isClientError) {
 | 
				
			||||||
						ctx.status = err.statusCode;
 | 
											reply.code(err.statusCode);
 | 
				
			||||||
						ctx.set('Cache-Control', 'max-age=86400');
 | 
											reply.header('Cache-Control', 'max-age=86400');
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						ctx.status = 500;
 | 
											reply.code(500);
 | 
				
			||||||
						ctx.set('Cache-Control', 'max-age=300');
 | 
											reply.header('Cache-Control', 'max-age=300');
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				} finally {
 | 
									} finally {
 | 
				
			||||||
					cleanup();
 | 
										cleanup();
 | 
				
			||||||
| 
						 | 
					@ -154,8 +150,8 @@ export class FileServerService {
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.status = 204;
 | 
								reply.code(204);
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=86400');
 | 
								reply.header('Cache-Control', 'max-age=86400');
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,18 +162,17 @@ export class FileServerService {
 | 
				
			||||||
				extname: ext ? `.${ext}` : undefined,
 | 
									extname: ext ? `.${ext}` : undefined,
 | 
				
			||||||
			}).toString();
 | 
								}).toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = this.internalStorageService.read(key);
 | 
								reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
 | 
				
			||||||
			ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
 | 
								reply.header('Cache-Control', 'max-age=31536000, immutable');
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=31536000, immutable');
 | 
								reply.header('Content-Disposition', contentDisposition('inline', filename));
 | 
				
			||||||
			ctx.set('Content-Disposition', contentDisposition('inline', filename));
 | 
								return this.internalStorageService.read(key);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			const readable = this.internalStorageService.read(file.accessKey!);
 | 
								const readable = this.internalStorageService.read(file.accessKey!);
 | 
				
			||||||
			readable.on('error', this.commonReadableHandlerGenerator(ctx));
 | 
								readable.on('error', this.commonReadableHandlerGenerator(reply));
 | 
				
			||||||
			ctx.body = readable;
 | 
								reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
 | 
				
			||||||
			ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
 | 
								reply.header('Cache-Control', 'max-age=31536000, immutable');
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=31536000, immutable');
 | 
								reply.header('Content-Disposition', contentDisposition('inline', file.name));
 | 
				
			||||||
			ctx.set('Content-Disposition', contentDisposition('inline', file.name));
 | 
								return readable;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import * as fs from 'node:fs';
 | 
					import * as fs from 'node:fs';
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Koa from 'koa';
 | 
					import { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
 | 
				
			||||||
import cors from '@koa/cors';
 | 
					 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import sharp from 'sharp';
 | 
					import sharp from 'sharp';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -31,32 +29,29 @@ export class MediaProxyServerService {
 | 
				
			||||||
		private loggerService: LoggerService,
 | 
							private loggerService: LoggerService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		this.logger = this.loggerService.getLogger('server', 'gray', false);
 | 
							this.logger = this.loggerService.getLogger('server', 'gray', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createServer() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const app = new Koa();
 | 
							fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
		app.use(cors());
 | 
								reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
 | 
				
			||||||
		app.use(async (ctx, next) => {
 | 
								done();
 | 
				
			||||||
			ctx.set('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init router
 | 
							fastify.get<{
 | 
				
			||||||
		const router = new Router();
 | 
								Params: { url: string; };
 | 
				
			||||||
 | 
								Querystring: { url?: string; };
 | 
				
			||||||
 | 
							}>('/:url*', async (request, reply) => await this.handler(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/:url*', ctx => this.handler(ctx));
 | 
							done();
 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Register router
 | 
					 | 
				
			||||||
		app.use(router.routes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return app;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async handler(ctx: Koa.Context) {
 | 
						private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
 | 
				
			||||||
		const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
 | 
							const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (typeof url !== 'string') {
 | 
							if (typeof url !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -71,11 +66,11 @@ export class MediaProxyServerService {
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			let image: IImage;
 | 
								let image: IImage;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			if ('static' in ctx.query && isConvertibleImage) {
 | 
								if ('static' in request.query && isConvertibleImage) {
 | 
				
			||||||
				image = await this.imageProcessingService.convertToWebp(path, 498, 280);
 | 
									image = await this.imageProcessingService.convertToWebp(path, 498, 280);
 | 
				
			||||||
			} else if ('preview' in ctx.query && isConvertibleImage) {
 | 
								} else if ('preview' in request.query && isConvertibleImage) {
 | 
				
			||||||
				image = await this.imageProcessingService.convertToWebp(path, 200, 200);
 | 
									image = await this.imageProcessingService.convertToWebp(path, 200, 200);
 | 
				
			||||||
			} else if ('badge' in ctx.query) {
 | 
								} else if ('badge' in request.query) {
 | 
				
			||||||
				if (!isConvertibleImage) {
 | 
									if (!isConvertibleImage) {
 | 
				
			||||||
					// 画像でないなら404でお茶を濁す
 | 
										// 画像でないなら404でお茶を濁す
 | 
				
			||||||
					throw new StatusError('Unexpected mime', 404);
 | 
										throw new StatusError('Unexpected mime', 404);
 | 
				
			||||||
| 
						 | 
					@ -122,16 +117,16 @@ export class MediaProxyServerService {
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.set('Content-Type', image.type);
 | 
								reply.header('Content-Type', image.type);
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=31536000, immutable');
 | 
								reply.header('Cache-Control', 'max-age=31536000, immutable');
 | 
				
			||||||
			ctx.body = image.data;
 | 
								return image.data;
 | 
				
			||||||
		} catch (err) {
 | 
							} catch (err) {
 | 
				
			||||||
			this.logger.error(`${err}`);
 | 
								this.logger.error(`${err}`);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
 | 
								if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
 | 
				
			||||||
				ctx.status = err.statusCode;
 | 
									reply.code(err.statusCode);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 500;
 | 
									reply.code(500);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} finally {
 | 
							} finally {
 | 
				
			||||||
			cleanup();
 | 
								cleanup();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					import { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
 | 
				
			||||||
import { IsNull, MoreThan } from 'typeorm';
 | 
					import { IsNull, MoreThan } from 'typeorm';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { NotesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { NotesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ export class NodeinfoServerService {
 | 
				
			||||||
		private userEntityService: UserEntityService,
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public getLinks() {
 | 
						public getLinks() {
 | 
				
			||||||
| 
						 | 
					@ -39,9 +40,7 @@ export class NodeinfoServerService {
 | 
				
			||||||
			}];
 | 
								}];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createRouter() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const router = new Router();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const nodeinfo2 = async () => {
 | 
							const nodeinfo2 = async () => {
 | 
				
			||||||
			const now = Date.now();
 | 
								const now = Date.now();
 | 
				
			||||||
			const [
 | 
								const [
 | 
				
			||||||
| 
						 | 
					@ -108,22 +107,22 @@ export class NodeinfoServerService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
 | 
							const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get(nodeinfo2_1path, async ctx => {
 | 
							fastify.get(nodeinfo2_1path, async (request, reply) => {
 | 
				
			||||||
			const base = await cache.fetch(null, () => nodeinfo2());
 | 
								const base = await cache.fetch(null, () => nodeinfo2());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = { version: '2.1', ...base };
 | 
								reply.header('Cache-Control', 'public, max-age=600');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=600');
 | 
								return { version: '2.1', ...base };
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get(nodeinfo2_0path, async ctx => {
 | 
							fastify.get(nodeinfo2_0path, async (request, reply) => {
 | 
				
			||||||
			const base = await cache.fetch(null, () => nodeinfo2());
 | 
								const base = await cache.fetch(null, () => nodeinfo2());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			delete (base as any).software.repository;
 | 
								delete (base as any).software.repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = { version: '2.0', ...base };
 | 
								reply.header('Cache-Control', 'public, max-age=600');
 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=600');
 | 
								return { version: '2.0', ...base };
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return router;
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,7 @@ import cluster from 'node:cluster';
 | 
				
			||||||
import * as fs from 'node:fs';
 | 
					import * as fs from 'node:fs';
 | 
				
			||||||
import * as http from 'node:http';
 | 
					import * as http from 'node:http';
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Koa from 'koa';
 | 
					import Fastify from 'fastify';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import mount from 'koa-mount';
 | 
					 | 
				
			||||||
import koaLogger from 'koa-logger';
 | 
					 | 
				
			||||||
import * as slow from 'koa-slow';
 | 
					 | 
				
			||||||
import { IsNull } from 'typeorm';
 | 
					import { IsNull } from 'typeorm';
 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -58,47 +54,29 @@ export class ServerService {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public launch() {
 | 
						public launch() {
 | 
				
			||||||
		// Init app
 | 
							const fastify = Fastify({
 | 
				
			||||||
		const koa = new Koa();
 | 
								trustProxy: true,
 | 
				
			||||||
		koa.proxy = true;
 | 
								logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		if (!['production', 'test'].includes(process.env.NODE_ENV ?? '')) {
 | 
					 | 
				
			||||||
		// Logger
 | 
					 | 
				
			||||||
			koa.use(koaLogger(str => {
 | 
					 | 
				
			||||||
				this.logger.info(str);
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Delay
 | 
					 | 
				
			||||||
			if (envOption.slow) {
 | 
					 | 
				
			||||||
				koa.use(slow({
 | 
					 | 
				
			||||||
					delay: 3000,
 | 
					 | 
				
			||||||
				}));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// HSTS
 | 
							// HSTS
 | 
				
			||||||
		// 6months (15552000sec)
 | 
							// 6months (15552000sec)
 | 
				
			||||||
		if (this.config.url.startsWith('https') && !this.config.disableHsts) {
 | 
							if (this.config.url.startsWith('https') && !this.config.disableHsts) {
 | 
				
			||||||
			koa.use(async (ctx, next) => {
 | 
								fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
				ctx.set('strict-transport-security', 'max-age=15552000; preload');
 | 
									reply.header('strict-transport-security', 'max-age=15552000; preload');
 | 
				
			||||||
				await next();
 | 
									done();
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		koa.use(mount('/api', this.apiServerService.createApiServer(koa)));
 | 
							fastify.register(this.apiServerService.createServer, { prefix: '/api' });
 | 
				
			||||||
		koa.use(mount('/files', this.fileServerService.createServer()));
 | 
							fastify.register(this.fileServerService.createServer, { prefix: '/files' });
 | 
				
			||||||
		koa.use(mount('/proxy', this.mediaProxyServerService.createServer()));
 | 
							fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
 | 
				
			||||||
 | 
							fastify.register(this.activityPubServerService.createServer);
 | 
				
			||||||
 | 
							fastify.register(this.nodeinfoServerService.createServer);
 | 
				
			||||||
 | 
							fastify.register(this.wellKnownServerService.createServer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init router
 | 
							fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
 | 
				
			||||||
		const router = new Router();
 | 
								const { username, host } = Acct.parse(request.params.acct);
 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Routing
 | 
					 | 
				
			||||||
		router.use(this.activityPubServerService.createRouter().routes());
 | 
					 | 
				
			||||||
		router.use(this.nodeinfoServerService.createRouter().routes());
 | 
					 | 
				
			||||||
		router.use(this.wellKnownServerService.createRouter().routes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		router.get('/avatar/@:acct', async ctx => {
 | 
					 | 
				
			||||||
			const { username, host } = Acct.parse(ctx.params.acct);
 | 
					 | 
				
			||||||
			const user = await this.usersRepository.findOne({
 | 
								const user = await this.usersRepository.findOne({
 | 
				
			||||||
				where: {
 | 
									where: {
 | 
				
			||||||
					usernameLower: username.toLowerCase(),
 | 
										usernameLower: username.toLowerCase(),
 | 
				
			||||||
| 
						 | 
					@ -109,28 +87,25 @@ export class ServerService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (user) {
 | 
								if (user) {
 | 
				
			||||||
				ctx.redirect(this.userEntityService.getAvatarUrlSync(user));
 | 
									reply.redirect(this.userEntityService.getAvatarUrlSync(user));
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.redirect('/static-assets/user-unknown.png');
 | 
									reply.redirect('/static-assets/user-unknown.png');
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/identicon/:x', async ctx => {
 | 
							fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => {
 | 
				
			||||||
			const [temp, cleanup] = await createTemp();
 | 
								const [temp, cleanup] = await createTemp();
 | 
				
			||||||
			await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
 | 
								await genIdenticon(request.params.x, fs.createWriteStream(temp));
 | 
				
			||||||
			ctx.set('Content-Type', 'image/png');
 | 
								reply.header('Content-Type', 'image/png');
 | 
				
			||||||
			ctx.body = fs.createReadStream(temp).on('close', () => cleanup());
 | 
								return fs.createReadStream(temp).on('close', () => cleanup());
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/verify-email/:code', async ctx => {
 | 
							fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {
 | 
				
			||||||
			const profile = await this.userProfilesRepository.findOneBy({
 | 
								const profile = await this.userProfilesRepository.findOneBy({
 | 
				
			||||||
				emailVerifyCode: ctx.params.code,
 | 
									emailVerifyCode: request.params.code,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (profile != null) {
 | 
								if (profile != null) {
 | 
				
			||||||
				ctx.body = 'Verify succeeded!';
 | 
					 | 
				
			||||||
				ctx.status = 200;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				await this.userProfilesRepository.update({ userId: profile.userId }, {
 | 
									await this.userProfilesRepository.update({ userId: profile.userId }, {
 | 
				
			||||||
					emailVerified: true,
 | 
										emailVerified: true,
 | 
				
			||||||
					emailVerifyCode: null,
 | 
										emailVerifyCode: null,
 | 
				
			||||||
| 
						 | 
					@ -140,21 +115,19 @@ export class ServerService {
 | 
				
			||||||
					detail: true,
 | 
										detail: true,
 | 
				
			||||||
					includeSecrets: true,
 | 
										includeSecrets: true,
 | 
				
			||||||
				}));
 | 
									}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									reply.code(200);
 | 
				
			||||||
 | 
									return 'Verify succeeded!';
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Register router
 | 
							fastify.register(this.clientServerService.createServer);
 | 
				
			||||||
		koa.use(router.routes());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		koa.use(mount(this.clientServerService.createApp()));
 | 
							this.streamingApiServerService.attachStreamingApi(fastify.server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const server = http.createServer(koa.callback());
 | 
							fastify.server.on('error', err => {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.streamingApiServerService.attachStreamingApi(server);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		server.on('error', err => {
 | 
					 | 
				
			||||||
			switch ((err as any).code) {
 | 
								switch ((err as any).code) {
 | 
				
			||||||
				case 'EACCES':
 | 
									case 'EACCES':
 | 
				
			||||||
					this.logger.error(`You do not have permission to listen on port ${this.config.port}.`);
 | 
										this.logger.error(`You do not have permission to listen on port ${this.config.port}.`);
 | 
				
			||||||
| 
						 | 
					@ -168,13 +141,13 @@ export class ServerService {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (cluster.isWorker) {
 | 
								if (cluster.isWorker) {
 | 
				
			||||||
			process.send!('listenFailed');
 | 
									process.send!('listenFailed');
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
			// disableClustering
 | 
									// disableClustering
 | 
				
			||||||
				process.exit(1);
 | 
									process.exit(1);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		server.listen(this.config.port);
 | 
							fastify.listen({ port: this.config.port });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					import { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
 | 
				
			||||||
import { IsNull, MoreThan } from 'typeorm';
 | 
					import { IsNull, MoreThan } from 'typeorm';
 | 
				
			||||||
 | 
					import vary from 'vary';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { UsersRepository } from '@/models/index.js';
 | 
					import type { UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -21,11 +22,10 @@ export class WellKnownServerService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private nodeinfoServerService: NodeinfoServerService,
 | 
							private nodeinfoServerService: NodeinfoServerService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createRouter() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const router = new Router();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const XRD = (...x: { element: string, value?: string, attributes?: Record<string, string> }[]) =>
 | 
							const XRD = (...x: { element: string, value?: string, attributes?: Record<string, string> }[]) =>
 | 
				
			||||||
			`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x.map(({ element, value, attributes }) =>
 | 
								`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x.map(({ element, value, attributes }) =>
 | 
				
			||||||
				`<${
 | 
									`<${
 | 
				
			||||||
| 
						 | 
					@ -34,37 +34,35 @@ export class WellKnownServerService {
 | 
				
			||||||
					typeof value === 'string' ? `>${escapeValue(value)}</${element}` : '/'
 | 
										typeof value === 'string' ? `>${escapeValue(value)}</${element}` : '/'
 | 
				
			||||||
				}>`).reduce((a, c) => a + c, '')}</XRD>`;
 | 
									}>`).reduce((a, c) => a + c, '')}</XRD>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const allPath = '/.well-known/(.*)';
 | 
							const allPath = '/.well-known/*';
 | 
				
			||||||
		const webFingerPath = '/.well-known/webfinger';
 | 
							const webFingerPath = '/.well-known/webfinger';
 | 
				
			||||||
		const jrd = 'application/jrd+json';
 | 
							const jrd = 'application/jrd+json';
 | 
				
			||||||
		const xrd = 'application/xrd+xml';
 | 
							const xrd = 'application/xrd+xml';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.use(allPath, async (ctx, next) => {
 | 
							fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
			ctx.set({
 | 
								reply.header('Access-Control-Allow-Headers', 'Accept');
 | 
				
			||||||
				'Access-Control-Allow-Headers': 'Accept',
 | 
								reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
 | 
				
			||||||
				'Access-Control-Allow-Methods': 'GET, OPTIONS',
 | 
								reply.header('Access-Control-Allow-Origin', '*');
 | 
				
			||||||
				'Access-Control-Allow-Origin': '*',
 | 
								reply.header('Access-Control-Expose-Headers', 'Vary');
 | 
				
			||||||
				'Access-Control-Expose-Headers': 'Vary',
 | 
								done();
 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.options(allPath, async ctx => {
 | 
							fastify.options(allPath, async (request, reply) => {
 | 
				
			||||||
			ctx.status = 204;
 | 
								reply.code(204);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/.well-known/host-meta', async ctx => {
 | 
							fastify.get('/.well-known/host-meta', async (request, reply) => {
 | 
				
			||||||
			ctx.set('Content-Type', xrd);
 | 
								reply.header('Content-Type', xrd);
 | 
				
			||||||
			ctx.body = XRD({ element: 'Link', attributes: {
 | 
								return XRD({ element: 'Link', attributes: {
 | 
				
			||||||
				rel: 'lrdd',
 | 
									rel: 'lrdd',
 | 
				
			||||||
				type: xrd,
 | 
									type: xrd,
 | 
				
			||||||
				template: `${this.config.url}${webFingerPath}?resource={uri}`,
 | 
									template: `${this.config.url}${webFingerPath}?resource={uri}`,
 | 
				
			||||||
			} });
 | 
								} });
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/.well-known/host-meta.json', async ctx => {
 | 
							fastify.get('/.well-known/host-meta.json', async (request, reply) => {
 | 
				
			||||||
			ctx.set('Content-Type', jrd);
 | 
								reply.header('Content-Type', jrd);
 | 
				
			||||||
			ctx.body = {
 | 
								return {
 | 
				
			||||||
				links: [{
 | 
									links: [{
 | 
				
			||||||
					rel: 'lrdd',
 | 
										rel: 'lrdd',
 | 
				
			||||||
					type: jrd,
 | 
										type: jrd,
 | 
				
			||||||
| 
						 | 
					@ -73,16 +71,16 @@ export class WellKnownServerService {
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/.well-known/nodeinfo', async ctx => {
 | 
							fastify.get('/.well-known/nodeinfo', async (request, reply) => {
 | 
				
			||||||
			ctx.body = { links: this.nodeinfoServerService.getLinks() };
 | 
								return { links: this.nodeinfoServerService.getLinks() };
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/* TODO
 | 
							/* TODO
 | 
				
			||||||
router.get('/.well-known/change-password', async ctx => {
 | 
					fastify.get('/.well-known/change-password', async (request, reply) => {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get(webFingerPath, async ctx => {
 | 
							fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => {
 | 
				
			||||||
			const fromId = (id: User['id']): FindOptionsWhere<User> => ({
 | 
								const fromId = (id: User['id']): FindOptionsWhere<User> => ({
 | 
				
			||||||
				id,
 | 
									id,
 | 
				
			||||||
				host: IsNull(),
 | 
									host: IsNull(),
 | 
				
			||||||
| 
						 | 
					@ -104,22 +102,22 @@ router.get('/.well-known/change-password', async ctx => {
 | 
				
			||||||
					isSuspended: false,
 | 
										isSuspended: false,
 | 
				
			||||||
				} : 422;
 | 
									} : 422;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (typeof ctx.query.resource !== 'string') {
 | 
								if (typeof request.query.resource !== 'string') {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const query = generateQuery(ctx.query.resource.toLowerCase());
 | 
								const query = generateQuery(request.query.resource.toLowerCase());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (typeof query === 'number') {
 | 
								if (typeof query === 'number') {
 | 
				
			||||||
				ctx.status = query;
 | 
									reply.code(query);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy(query);
 | 
								const user = await this.usersRepository.findOneBy(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (user == null) {
 | 
								if (user == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -139,30 +137,25 @@ router.get('/.well-known/change-password', async ctx => {
 | 
				
			||||||
				template: `${this.config.url}/authorize-follow?acct={uri}`,
 | 
									template: `${this.config.url}/authorize-follow?acct={uri}`,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (ctx.accepts(jrd, xrd) === xrd) {
 | 
								vary(reply.raw, 'Accept');
 | 
				
			||||||
				ctx.body = XRD(
 | 
								reply.header('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (request.accepts().type([jrd, xrd]) === xrd) {
 | 
				
			||||||
 | 
									reply.type(xrd);
 | 
				
			||||||
 | 
									return XRD(
 | 
				
			||||||
					{ element: 'Subject', value: subject },
 | 
										{ element: 'Subject', value: subject },
 | 
				
			||||||
					{ element: 'Link', attributes: self },
 | 
										{ element: 'Link', attributes: self },
 | 
				
			||||||
					{ element: 'Link', attributes: profilePage },
 | 
										{ element: 'Link', attributes: profilePage },
 | 
				
			||||||
					{ element: 'Link', attributes: subscribe });
 | 
										{ element: 'Link', attributes: subscribe });
 | 
				
			||||||
				ctx.type = xrd;
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.body = {
 | 
									reply.type(jrd);
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
					subject,
 | 
										subject,
 | 
				
			||||||
					links: [self, profilePage, subscribe],
 | 
										links: [self, profilePage, subscribe],
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
				ctx.type = jrd;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			ctx.vary('Accept');
 | 
					 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=180');
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Return 404 for other .well-known
 | 
							done();
 | 
				
			||||||
		router.all(allPath, async ctx => {
 | 
					 | 
				
			||||||
			ctx.status = 404;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return router;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,25 @@
 | 
				
			||||||
import { performance } from 'perf_hooks';
 | 
					import { performance } from 'perf_hooks';
 | 
				
			||||||
 | 
					import { pipeline } from 'node:stream';
 | 
				
			||||||
 | 
					import * as fs from 'node:fs';
 | 
				
			||||||
 | 
					import { promisify } from 'node:util';
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { FastifyRequest, FastifyReply } from 'fastify';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
 | 
					import { getIpHash } from '@/misc/get-ip-hash.js';
 | 
				
			||||||
import type { CacheableLocalUser, User } from '@/models/entities/User.js';
 | 
					import type { CacheableLocalUser, ILocalUser, User } from '@/models/entities/User.js';
 | 
				
			||||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
 | 
					import type { AccessToken } from '@/models/entities/AccessToken.js';
 | 
				
			||||||
import type Logger from '@/logger.js';
 | 
					import type Logger from '@/logger.js';
 | 
				
			||||||
import type { UserIpsRepository } from '@/models/index.js';
 | 
					import type { UserIpsRepository } from '@/models/index.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
 | 
					import { createTemp } from '@/misc/create-temp.js';
 | 
				
			||||||
import { ApiError } from './error.js';
 | 
					import { ApiError } from './error.js';
 | 
				
			||||||
import { RateLimiterService } from './RateLimiterService.js';
 | 
					import { RateLimiterService } from './RateLimiterService.js';
 | 
				
			||||||
import { ApiLoggerService } from './ApiLoggerService.js';
 | 
					import { ApiLoggerService } from './ApiLoggerService.js';
 | 
				
			||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
 | 
					import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
 | 
				
			||||||
import type { OnApplicationShutdown } from '@nestjs/common';
 | 
					import type { OnApplicationShutdown } from '@nestjs/common';
 | 
				
			||||||
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
 | 
					import type { IEndpointMeta, IEndpoint } from './endpoints.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					
 | 
				
			||||||
 | 
					const pump = promisify(pipeline);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accessDenied = {
 | 
					const accessDenied = {
 | 
				
			||||||
	message: 'Access denied.',
 | 
						message: 'Access denied.',
 | 
				
			||||||
| 
						 | 
					@ -44,92 +50,149 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
				
			||||||
		}, 1000 * 60 * 60);
 | 
							}, 1000 * 60 * 60);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public handleRequest(endpoint: IEndpoint, exec: any, ctx: Koa.Context) {
 | 
						public handleRequest(
 | 
				
			||||||
		return new Promise<void>((res) => {
 | 
							endpoint: IEndpoint & { exec: any },
 | 
				
			||||||
			const body = ctx.is('multipart/form-data')
 | 
							request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
 | 
				
			||||||
				? (ctx.request as any).body
 | 
							reply: FastifyReply,
 | 
				
			||||||
				: ctx.method === 'GET'
 | 
						) {
 | 
				
			||||||
					? ctx.query
 | 
							const body = request.method === 'GET'
 | 
				
			||||||
					: ctx.request.body;
 | 
								? request.query
 | 
				
			||||||
 | 
								: request.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const reply = (x?: any, y?: ApiError) => {
 | 
							const token = body['i'];
 | 
				
			||||||
				if (x == null) {
 | 
							if (token != null && typeof token !== 'string') {
 | 
				
			||||||
					ctx.status = 204;
 | 
								reply.code(400);
 | 
				
			||||||
				} else if (typeof x === 'number' && y) {
 | 
								return;
 | 
				
			||||||
					ctx.status = x;
 | 
							}
 | 
				
			||||||
					ctx.body = {
 | 
							this.authenticateService.authenticate(token).then(([user, app]) => {
 | 
				
			||||||
						error: {
 | 
								this.call(endpoint, user, app, body, null, request).then((res) => {
 | 
				
			||||||
							message: y!.message,
 | 
									if (request.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
 | 
				
			||||||
							code: y!.code,
 | 
										reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
 | 
				
			||||||
							id: y!.id,
 | 
					 | 
				
			||||||
							kind: y!.kind,
 | 
					 | 
				
			||||||
							...(y!.info ? { info: y!.info } : {}),
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
 | 
					 | 
				
			||||||
					ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				res();
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
			// Authentication
 | 
					 | 
				
			||||||
			this.authenticateService.authenticate(body['i']).then(([user, app]) => {
 | 
					 | 
				
			||||||
				// API invoking
 | 
					 | 
				
			||||||
				this.call(endpoint, exec, user, app, body, ctx).then((res: any) => {
 | 
					 | 
				
			||||||
					if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
 | 
					 | 
				
			||||||
						ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					reply(res);
 | 
					 | 
				
			||||||
				}).catch((e: ApiError) => {
 | 
					 | 
				
			||||||
					reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
				// Log IP
 | 
					 | 
				
			||||||
				if (user) {
 | 
					 | 
				
			||||||
					this.metaService.fetch().then(meta => {
 | 
					 | 
				
			||||||
						if (!meta.enableIpLogging) return;
 | 
					 | 
				
			||||||
						const ip = ctx.ip;
 | 
					 | 
				
			||||||
						const ips = this.userIpHistories.get(user.id);
 | 
					 | 
				
			||||||
						if (ips == null || !ips.has(ip)) {
 | 
					 | 
				
			||||||
							if (ips == null) {
 | 
					 | 
				
			||||||
								this.userIpHistories.set(user.id, new Set([ip]));
 | 
					 | 
				
			||||||
							} else {
 | 
					 | 
				
			||||||
								ips.add(ip);
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
							try {
 | 
					 | 
				
			||||||
								this.userIpsRepository.createQueryBuilder().insert().values({
 | 
					 | 
				
			||||||
									createdAt: new Date(),
 | 
					 | 
				
			||||||
									userId: user.id,
 | 
					 | 
				
			||||||
									ip: ip,
 | 
					 | 
				
			||||||
								}).orIgnore(true).execute();
 | 
					 | 
				
			||||||
							} catch {
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}).catch(e => {
 | 
					 | 
				
			||||||
				if (e instanceof AuthenticationError) {
 | 
					 | 
				
			||||||
					reply(403, new ApiError({
 | 
					 | 
				
			||||||
						message: 'Authentication failed. Please ensure your token is correct.',
 | 
					 | 
				
			||||||
						code: 'AUTHENTICATION_FAILED',
 | 
					 | 
				
			||||||
						id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
 | 
					 | 
				
			||||||
					}));
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					reply(500, new ApiError());
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									this.send(reply, res);
 | 
				
			||||||
 | 
								}).catch((err: ApiError) => {
 | 
				
			||||||
 | 
									this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (user) {
 | 
				
			||||||
 | 
									this.logIp(request, user);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}).catch(err => {
 | 
				
			||||||
 | 
								if (err instanceof AuthenticationError) {
 | 
				
			||||||
 | 
									this.send(reply, 403, new ApiError({
 | 
				
			||||||
 | 
										message: 'Authentication failed. Please ensure your token is correct.',
 | 
				
			||||||
 | 
										code: 'AUTHENTICATION_FAILED',
 | 
				
			||||||
 | 
										id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
 | 
				
			||||||
 | 
									}));
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.send(reply, 500, new ApiError());
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async handleMultipartRequest(
 | 
				
			||||||
 | 
							endpoint: IEndpoint & { exec: any },
 | 
				
			||||||
 | 
							request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const multipartData = await request.file();
 | 
				
			||||||
 | 
							if (multipartData == null) {
 | 
				
			||||||
 | 
								reply.code(400);
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const [path] = await createTemp();
 | 
				
			||||||
 | 
							await pump(multipartData.file, fs.createWriteStream(path));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fields = {} as Record<string, string | undefined>;
 | 
				
			||||||
 | 
							for (const [k, v] of Object.entries(multipartData.fields)) {
 | 
				
			||||||
 | 
								fields[k] = v.value;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							const token = fields['i'];
 | 
				
			||||||
 | 
							if (token != null && typeof token !== 'string') {
 | 
				
			||||||
 | 
								reply.code(400);
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.authenticateService.authenticate(token).then(([user, app]) => {
 | 
				
			||||||
 | 
								this.call(endpoint, user, app, fields, {
 | 
				
			||||||
 | 
									name: multipartData.filename,
 | 
				
			||||||
 | 
									path: path,
 | 
				
			||||||
 | 
								}, request).then((res) => {
 | 
				
			||||||
 | 
									this.send(reply, res);
 | 
				
			||||||
 | 
								}).catch((err: ApiError) => {
 | 
				
			||||||
 | 
									this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (user) {
 | 
				
			||||||
 | 
									this.logIp(request, user);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}).catch(err => {
 | 
				
			||||||
 | 
								if (err instanceof AuthenticationError) {
 | 
				
			||||||
 | 
									this.send(reply, 403, new ApiError({
 | 
				
			||||||
 | 
										message: 'Authentication failed. Please ensure your token is correct.',
 | 
				
			||||||
 | 
										code: 'AUTHENTICATION_FAILED',
 | 
				
			||||||
 | 
										id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
 | 
				
			||||||
 | 
									}));
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.send(reply, 500, new ApiError());
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private send(reply: FastifyReply, x?: any, y?: ApiError) {
 | 
				
			||||||
 | 
							if (x == null) {
 | 
				
			||||||
 | 
								reply.code(204);
 | 
				
			||||||
 | 
							} else if (typeof x === 'number' && y) {
 | 
				
			||||||
 | 
								reply.code(x);
 | 
				
			||||||
 | 
								reply.send({
 | 
				
			||||||
 | 
									error: {
 | 
				
			||||||
 | 
										message: y!.message,
 | 
				
			||||||
 | 
										code: y!.code,
 | 
				
			||||||
 | 
										id: y!.id,
 | 
				
			||||||
 | 
										kind: y!.kind,
 | 
				
			||||||
 | 
										...(y!.info ? { info: y!.info } : {}),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
 | 
				
			||||||
 | 
								reply.send(typeof x === 'string' ? JSON.stringify(x) : x);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async logIp(request: FastifyRequest, user: ILocalUser) {
 | 
				
			||||||
 | 
							const meta = await this.metaService.fetch();
 | 
				
			||||||
 | 
							if (!meta.enableIpLogging) return;
 | 
				
			||||||
 | 
							const ip = request.ip;
 | 
				
			||||||
 | 
							const ips = this.userIpHistories.get(user.id);
 | 
				
			||||||
 | 
							if (ips == null || !ips.has(ip)) {
 | 
				
			||||||
 | 
								if (ips == null) {
 | 
				
			||||||
 | 
									this.userIpHistories.set(user.id, new Set([ip]));
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									ips.add(ip);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									this.userIpsRepository.createQueryBuilder().insert().values({
 | 
				
			||||||
 | 
										createdAt: new Date(),
 | 
				
			||||||
 | 
										userId: user.id,
 | 
				
			||||||
 | 
										ip: ip,
 | 
				
			||||||
 | 
									}).orIgnore(true).execute();
 | 
				
			||||||
 | 
								} catch {
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async call(
 | 
						private async call(
 | 
				
			||||||
		ep: IEndpoint,
 | 
							ep: IEndpoint & { exec: any },
 | 
				
			||||||
		exec: any,
 | 
					 | 
				
			||||||
		user: CacheableLocalUser | null | undefined,
 | 
							user: CacheableLocalUser | null | undefined,
 | 
				
			||||||
		token: AccessToken | null | undefined,
 | 
							token: AccessToken | null | undefined,
 | 
				
			||||||
		data: any,
 | 
							data: any,
 | 
				
			||||||
		ctx?: Koa.Context,
 | 
							file: {
 | 
				
			||||||
 | 
								name: string;
 | 
				
			||||||
 | 
								path: string;
 | 
				
			||||||
 | 
							} | null,
 | 
				
			||||||
 | 
							request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		const isSecure = user != null && token == null;
 | 
							const isSecure = user != null && token == null;
 | 
				
			||||||
		const isModerator = user != null && (user.isModerator || user.isAdmin);
 | 
							const isModerator = user != null && (user.isModerator || user.isAdmin);
 | 
				
			||||||
| 
						 | 
					@ -144,7 +207,7 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
				
			||||||
			if (user) {
 | 
								if (user) {
 | 
				
			||||||
				limitActor = user.id;
 | 
									limitActor = user.id;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				limitActor = getIpHash(ctx!.ip);
 | 
									limitActor = getIpHash(request.ip);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const limit = Object.assign({}, ep.meta.limit);
 | 
								const limit = Object.assign({}, ep.meta.limit);
 | 
				
			||||||
| 
						 | 
					@ -154,7 +217,7 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Rate limit
 | 
								// Rate limit
 | 
				
			||||||
			await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
 | 
								await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(err => {
 | 
				
			||||||
				throw new ApiError({
 | 
									throw new ApiError({
 | 
				
			||||||
					message: 'Rate limit exceeded. Please try again later.',
 | 
										message: 'Rate limit exceeded. Please try again later.',
 | 
				
			||||||
					code: 'RATE_LIMIT_EXCEEDED',
 | 
										code: 'RATE_LIMIT_EXCEEDED',
 | 
				
			||||||
| 
						 | 
					@ -199,7 +262,7 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Cast non JSON input
 | 
							// Cast non JSON input
 | 
				
			||||||
		if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
 | 
							if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
 | 
				
			||||||
			for (const k of Object.keys(ep.params.properties)) {
 | 
								for (const k of Object.keys(ep.params.properties)) {
 | 
				
			||||||
				const param = ep.params.properties![k];
 | 
									const param = ep.params.properties![k];
 | 
				
			||||||
				if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
 | 
									if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
 | 
				
			||||||
| 
						 | 
					@ -221,7 +284,7 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// API invoking
 | 
							// API invoking
 | 
				
			||||||
		const before = performance.now();
 | 
							const before = performance.now();
 | 
				
			||||||
		return await exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((err: Error) => {
 | 
							return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
 | 
				
			||||||
			if (err instanceof ApiError) {
 | 
								if (err instanceof ApiError) {
 | 
				
			||||||
				throw err;
 | 
									throw err;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,13 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Koa from 'koa';
 | 
					import { FastifyInstance, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					import cors from '@fastify/cors';
 | 
				
			||||||
import multer from '@koa/multer';
 | 
					import multipart from '@fastify/multipart';
 | 
				
			||||||
import bodyParser from 'koa-bodyparser';
 | 
					import { ModuleRef, repl } from '@nestjs/core';
 | 
				
			||||||
import cors from '@koa/cors';
 | 
					 | 
				
			||||||
import { ModuleRef } from '@nestjs/core';
 | 
					 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js';
 | 
					import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
import endpoints from './endpoints.js';
 | 
					import endpoints, { IEndpoint } from './endpoints.js';
 | 
				
			||||||
import { ApiCallService } from './ApiCallService.js';
 | 
					import { ApiCallService } from './ApiCallService.js';
 | 
				
			||||||
import { SignupApiService } from './SignupApiService.js';
 | 
					import { SignupApiService } from './SignupApiService.js';
 | 
				
			||||||
import { SigninApiService } from './SigninApiService.js';
 | 
					import { SigninApiService } from './SigninApiService.js';
 | 
				
			||||||
| 
						 | 
					@ -42,92 +40,107 @@ export class ApiServerService {
 | 
				
			||||||
		private discordServerService: DiscordServerService,
 | 
							private discordServerService: DiscordServerService,
 | 
				
			||||||
		private twitterServerService: TwitterServerService,
 | 
							private twitterServerService: TwitterServerService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createApiServer() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const handlers: Record<string, any> = {};
 | 
							fastify.register(cors, {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		for (const endpoint of endpoints) {
 | 
					 | 
				
			||||||
			handlers[endpoint.name] = this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Init app
 | 
					 | 
				
			||||||
		const apiServer = new Koa();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		apiServer.use(cors({
 | 
					 | 
				
			||||||
			origin: '*',
 | 
								origin: '*',
 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// No caching
 | 
					 | 
				
			||||||
		apiServer.use(async (ctx, next) => {
 | 
					 | 
				
			||||||
			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		apiServer.use(bodyParser({
 | 
							fastify.register(multipart, {
 | 
				
			||||||
			// リクエストが multipart/form-data でない限りはJSONだと見なす
 | 
					 | 
				
			||||||
			detectJSON: ctx => !ctx.is('multipart/form-data'),
 | 
					 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Init multer instance
 | 
					 | 
				
			||||||
		const upload = multer({
 | 
					 | 
				
			||||||
			storage: multer.diskStorage({}),
 | 
					 | 
				
			||||||
			limits: {
 | 
								limits: {
 | 
				
			||||||
				fileSize: this.config.maxFileSize ?? 262144000,
 | 
									fileSize: this.config.maxFileSize ?? 262144000,
 | 
				
			||||||
				files: 1,
 | 
									files: 1,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init router
 | 
							// Prevent cache
 | 
				
			||||||
		const router = new Router();
 | 
							fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
 | 
								reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
 | 
				
			||||||
 | 
								done();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/**
 | 
					 | 
				
			||||||
		 * Register endpoint handlers
 | 
					 | 
				
			||||||
		 */
 | 
					 | 
				
			||||||
		for (const endpoint of endpoints) {
 | 
							for (const endpoint of endpoints) {
 | 
				
			||||||
 | 
								const ep = {
 | 
				
			||||||
 | 
									name: endpoint.name,
 | 
				
			||||||
 | 
									meta: endpoint.meta,
 | 
				
			||||||
 | 
									params: endpoint.params,
 | 
				
			||||||
 | 
									exec: this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec,
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (endpoint.meta.requireFile) {
 | 
								if (endpoint.meta.requireFile) {
 | 
				
			||||||
				router.post(`/${endpoint.name}`, upload.single('file'), this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
 | 
									fastify.all<{
 | 
				
			||||||
			} else {
 | 
										Params: { endpoint: string; },
 | 
				
			||||||
				// 後方互換性のため
 | 
										Body: Record<string, unknown>,
 | 
				
			||||||
				if (endpoint.name.includes('-')) {
 | 
										Querystring: Record<string, unknown>,
 | 
				
			||||||
					router.post(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
 | 
									}>('/' + endpoint.name, (request, reply) => {
 | 
				
			||||||
 | 
										if (request.method === 'GET' && !endpoint.meta.allowGet) {
 | 
				
			||||||
					if (endpoint.meta.allowGet) {
 | 
											reply.code(405);
 | 
				
			||||||
						router.get(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
 | 
											return;
 | 
				
			||||||
					} else {
 | 
					 | 
				
			||||||
						router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; });
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
				router.post(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
 | 
										this.apiCallService.handleMultipartRequest(ep, request, reply);
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									fastify.all<{
 | 
				
			||||||
 | 
										Params: { endpoint: string; },
 | 
				
			||||||
 | 
										Body: Record<string, unknown>,
 | 
				
			||||||
 | 
										Querystring: Record<string, unknown>,
 | 
				
			||||||
 | 
									}>('/' + endpoint.name, (request, reply) => {
 | 
				
			||||||
 | 
										if (request.method === 'GET' && !endpoint.meta.allowGet) {
 | 
				
			||||||
 | 
											reply.code(405);
 | 
				
			||||||
 | 
											return;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
				if (endpoint.meta.allowGet) {
 | 
										this.apiCallService.handleRequest(ep, request, reply);
 | 
				
			||||||
					router.get(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
 | 
									});
 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.post('/signup', ctx => this.signupApiServiceService.signup(ctx));
 | 
							fastify.post<{
 | 
				
			||||||
		router.post('/signin', ctx => this.signinApiServiceService.signin(ctx));
 | 
								Body: {
 | 
				
			||||||
		router.post('/signup-pending', ctx => this.signupApiServiceService.signupPending(ctx));
 | 
									username: string;
 | 
				
			||||||
 | 
									password: string;
 | 
				
			||||||
 | 
									host?: string;
 | 
				
			||||||
 | 
									invitationCode?: string;
 | 
				
			||||||
 | 
									emailAddress?: string;
 | 
				
			||||||
 | 
									'hcaptcha-response'?: string;
 | 
				
			||||||
 | 
									'g-recaptcha-response'?: string;
 | 
				
			||||||
 | 
									'turnstile-response'?: string;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}>('/signup', (request, reply) => this.signupApiServiceService.signup(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.use(this.discordServerService.create().routes());
 | 
							fastify.post<{
 | 
				
			||||||
		router.use(this.githubServerService.create().routes());
 | 
								Body: {
 | 
				
			||||||
		router.use(this.twitterServerService.create().routes());
 | 
									username: string;
 | 
				
			||||||
 | 
									password: string;
 | 
				
			||||||
 | 
									token?: string;
 | 
				
			||||||
 | 
									signature?: string;
 | 
				
			||||||
 | 
									authenticatorData?: string;
 | 
				
			||||||
 | 
									clientDataJSON?: string;
 | 
				
			||||||
 | 
									credentialId?: string;
 | 
				
			||||||
 | 
									challengeId?: string;
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}>('/signin', (request, reply) => this.signinApiServiceService.signin(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/v1/instance/peers', async ctx => {
 | 
							fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiServiceService.signupPending(request, reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fastify.register(this.discordServerService.create);
 | 
				
			||||||
 | 
							fastify.register(this.githubServerService.create);
 | 
				
			||||||
 | 
							fastify.register(this.twitterServerService.create);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fastify.get('/v1/instance/peers', async (request, reply) => {
 | 
				
			||||||
			const instances = await this.instancesRepository.find({
 | 
								const instances = await this.instancesRepository.find({
 | 
				
			||||||
				select: ['host'],
 | 
									select: ['host'],
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = instances.map(instance => instance.host);
 | 
								return instances.map(instance => instance.host);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.post('/miauth/:session/check', async ctx => {
 | 
							fastify.post<{ Params: { session: string; } }>('/miauth/:session/check', async (request, reply) => {
 | 
				
			||||||
			const token = await this.accessTokensRepository.findOneBy({
 | 
								const token = await this.accessTokensRepository.findOneBy({
 | 
				
			||||||
				session: ctx.params.session,
 | 
									session: request.params.session,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (token && token.session != null && !token.fetched) {
 | 
								if (token && token.session != null && !token.fetched) {
 | 
				
			||||||
| 
						 | 
					@ -135,26 +148,18 @@ export class ApiServerService {
 | 
				
			||||||
					fetched: true,
 | 
										fetched: true,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				ctx.body = {
 | 
									return {
 | 
				
			||||||
					ok: true,
 | 
										ok: true,
 | 
				
			||||||
					token: token.token,
 | 
										token: token.token,
 | 
				
			||||||
					user: await this.userEntityService.pack(token.userId, null, { detail: true }),
 | 
										user: await this.userEntityService.pack(token.userId, null, { detail: true }),
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.body = {
 | 
									return {
 | 
				
			||||||
					ok: false,
 | 
										ok: false,
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Return 404 for unknown API
 | 
							done();
 | 
				
			||||||
		router.all('(.*)', async ctx => {
 | 
					 | 
				
			||||||
			ctx.status = 404;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Register router
 | 
					 | 
				
			||||||
		apiServer.use(router.routes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return apiServer;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ export class AuthenticateService {
 | 
				
			||||||
		this.appCache = new Cache<App>(Infinity);
 | 
							this.appCache = new Cache<App>(Infinity);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async authenticate(token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> {
 | 
						public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> {
 | 
				
			||||||
		if (token == null) {
 | 
							if (token == null) {
 | 
				
			||||||
			return [null, null];
 | 
								return [null, null];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import bcrypt from 'bcryptjs';
 | 
					import bcrypt from 'bcryptjs';
 | 
				
			||||||
import * as speakeasy from 'speakeasy';
 | 
					import * as speakeasy from 'speakeasy';
 | 
				
			||||||
import { IsNull } from 'typeorm';
 | 
					import { IsNull } from 'typeorm';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -12,7 +13,6 @@ import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
 | 
					import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
 | 
				
			||||||
import { RateLimiterService } from './RateLimiterService.js';
 | 
					import { RateLimiterService } from './RateLimiterService.js';
 | 
				
			||||||
import { SigninService } from './SigninService.js';
 | 
					import { SigninService } from './SigninService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SigninApiService {
 | 
					export class SigninApiService {
 | 
				
			||||||
| 
						 | 
					@ -42,47 +42,60 @@ export class SigninApiService {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async signin(ctx: Koa.Context) {
 | 
						public async signin(
 | 
				
			||||||
		ctx.set('Access-Control-Allow-Origin', this.config.url);
 | 
							request: FastifyRequest<{
 | 
				
			||||||
		ctx.set('Access-Control-Allow-Credentials', 'true');
 | 
								Body: {
 | 
				
			||||||
 | 
									username: string;
 | 
				
			||||||
 | 
									password: string;
 | 
				
			||||||
 | 
									token?: string;
 | 
				
			||||||
 | 
									signature?: string;
 | 
				
			||||||
 | 
									authenticatorData?: string;
 | 
				
			||||||
 | 
									clientDataJSON?: string;
 | 
				
			||||||
 | 
									credentialId?: string;
 | 
				
			||||||
 | 
									challengeId?: string;
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							reply.header('Access-Control-Allow-Origin', this.config.url);
 | 
				
			||||||
 | 
							reply.header('Access-Control-Allow-Credentials', 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const body = ctx.request.body as any;
 | 
							const body = request.body;
 | 
				
			||||||
		const username = body['username'];
 | 
							const username = body['username'];
 | 
				
			||||||
		const password = body['password'];
 | 
							const password = body['password'];
 | 
				
			||||||
		const token = body['token'];
 | 
							const token = body['token'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		function error(status: number, error: { id: string }) {
 | 
							function error(status: number, error: { id: string }) {
 | 
				
			||||||
			ctx.status = status;
 | 
								reply.code(status);
 | 
				
			||||||
			ctx.body = { error };
 | 
								return { error };
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
		// not more than 1 attempt per second and not more than 10 attempts per hour
 | 
							// not more than 1 attempt per second and not more than 10 attempts per hour
 | 
				
			||||||
			await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip));
 | 
								await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
 | 
				
			||||||
		} catch (err) {
 | 
							} catch (err) {
 | 
				
			||||||
			ctx.status = 429;
 | 
								reply.code(429);
 | 
				
			||||||
			ctx.body = {
 | 
								return {
 | 
				
			||||||
				error: {
 | 
									error: {
 | 
				
			||||||
					message: 'Too many failed attempts to sign in. Try again later.',
 | 
										message: 'Too many failed attempts to sign in. Try again later.',
 | 
				
			||||||
					code: 'TOO_MANY_AUTHENTICATION_FAILURES',
 | 
										code: 'TOO_MANY_AUTHENTICATION_FAILURES',
 | 
				
			||||||
					id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
 | 
										id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (typeof username !== 'string') {
 | 
							if (typeof username !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (typeof password !== 'string') {
 | 
							if (typeof password !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (token != null && typeof token !== 'string') {
 | 
							if (token != null && typeof token !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,17 +106,15 @@ export class SigninApiService {
 | 
				
			||||||
		}) as ILocalUser;
 | 
							}) as ILocalUser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (user == null) {
 | 
							if (user == null) {
 | 
				
			||||||
			error(404, {
 | 
								return error(404, {
 | 
				
			||||||
				id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
 | 
									id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (user.isSuspended) {
 | 
							if (user.isSuspended) {
 | 
				
			||||||
			error(403, {
 | 
								return error(403, {
 | 
				
			||||||
				id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
 | 
									id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
							const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 | 
				
			||||||
| 
						 | 
					@ -117,32 +128,29 @@ export class SigninApiService {
 | 
				
			||||||
				id: this.idService.genId(),
 | 
									id: this.idService.genId(),
 | 
				
			||||||
				createdAt: new Date(),
 | 
									createdAt: new Date(),
 | 
				
			||||||
				userId: user.id,
 | 
									userId: user.id,
 | 
				
			||||||
				ip: ctx.ip,
 | 
									ip: request.ip,
 | 
				
			||||||
				headers: ctx.headers,
 | 
									headers: request.headers,
 | 
				
			||||||
				success: false,
 | 
									success: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
 | 
								return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!profile.twoFactorEnabled) {
 | 
							if (!profile.twoFactorEnabled) {
 | 
				
			||||||
			if (same) {
 | 
								if (same) {
 | 
				
			||||||
				this.signinService.signin(ctx, user);
 | 
									return this.signinService.signin(request, reply, user);
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
										id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (token) {
 | 
							if (token) {
 | 
				
			||||||
			if (!same) {
 | 
								if (!same) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
										id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const verified = (speakeasy as any).totp.verify({
 | 
								const verified = (speakeasy as any).totp.verify({
 | 
				
			||||||
| 
						 | 
					@ -153,20 +161,17 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (verified) {
 | 
								if (verified) {
 | 
				
			||||||
				this.signinService.signin(ctx, user);
 | 
									return this.signinService.signin(request, reply, user);
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
 | 
										id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else if (body.credentialId) {
 | 
							} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
 | 
				
			||||||
			if (!same && !profile.usePasswordLessLogin) {
 | 
								if (!same && !profile.usePasswordLessLogin) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
										id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
 | 
								const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
 | 
				
			||||||
| 
						 | 
					@ -179,10 +184,9 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!challenge) {
 | 
								if (!challenge) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '2715a88a-2125-4013-932f-aa6fe72792da',
 | 
										id: '2715a88a-2125-4013-932f-aa6fe72792da',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await this.attestationChallengesRepository.delete({
 | 
								await this.attestationChallengesRepository.delete({
 | 
				
			||||||
| 
						 | 
					@ -191,10 +195,9 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
 | 
								if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '2715a88a-2125-4013-932f-aa6fe72792da',
 | 
										id: '2715a88a-2125-4013-932f-aa6fe72792da',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const securityKey = await this.userSecurityKeysRepository.findOneBy({
 | 
								const securityKey = await this.userSecurityKeysRepository.findOneBy({
 | 
				
			||||||
| 
						 | 
					@ -207,10 +210,9 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!securityKey) {
 | 
								if (!securityKey) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '66269679-aeaf-4474-862b-eb761197e046',
 | 
										id: '66269679-aeaf-4474-862b-eb761197e046',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const isValid = this.twoFactorAuthenticationService.verifySignin({
 | 
								const isValid = this.twoFactorAuthenticationService.verifySignin({
 | 
				
			||||||
| 
						 | 
					@ -223,20 +225,17 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (isValid) {
 | 
								if (isValid) {
 | 
				
			||||||
				this.signinService.signin(ctx, user);
 | 
									return this.signinService.signin(request, reply, user);
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '93b86c4b-72f9-40eb-9815-798928603d1e',
 | 
										id: '93b86c4b-72f9-40eb-9815-798928603d1e',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			if (!same && !profile.usePasswordLessLogin) {
 | 
								if (!same && !profile.usePasswordLessLogin) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
										id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const keys = await this.userSecurityKeysRepository.findBy({
 | 
								const keys = await this.userSecurityKeysRepository.findBy({
 | 
				
			||||||
| 
						 | 
					@ -244,10 +243,9 @@ export class SigninApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (keys.length === 0) {
 | 
								if (keys.length === 0) {
 | 
				
			||||||
				await fail(403, {
 | 
									return await fail(403, {
 | 
				
			||||||
					id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
 | 
										id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 32 byte challenge
 | 
								// 32 byte challenge
 | 
				
			||||||
| 
						 | 
					@ -266,15 +264,14 @@ export class SigninApiService {
 | 
				
			||||||
				registrationChallenge: false,
 | 
									registrationChallenge: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = {
 | 
								reply.code(200);
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
				challenge,
 | 
									challenge,
 | 
				
			||||||
				challengeId,
 | 
									challengeId,
 | 
				
			||||||
				securityKeys: keys.map(key => ({
 | 
									securityKeys: keys.map(key => ({
 | 
				
			||||||
					id: key.id,
 | 
										id: key.id,
 | 
				
			||||||
				})),
 | 
									})),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			ctx.status = 200;
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	// never get here
 | 
						// never get here
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,12 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { SigninsRepository } from '@/models/index.js';
 | 
					import type { SigninsRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import type { UsersRepository } from '@/models/index.js';
 | 
					 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import { IdService } from '@/core/IdService.js';
 | 
					import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
import type { ILocalUser } from '@/models/entities/User.js';
 | 
					import type { ILocalUser } from '@/models/entities/User.js';
 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
 | 
					import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SigninService {
 | 
					export class SigninService {
 | 
				
			||||||
| 
						 | 
					@ -24,10 +23,25 @@ export class SigninService {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public signin(ctx: Koa.Context, user: ILocalUser, redirect = false) {
 | 
						public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
 | 
				
			||||||
 | 
							setImmediate(async () => {
 | 
				
			||||||
 | 
								// Append signin history
 | 
				
			||||||
 | 
								const record = await this.signinsRepository.insert({
 | 
				
			||||||
 | 
									id: this.idService.genId(),
 | 
				
			||||||
 | 
									createdAt: new Date(),
 | 
				
			||||||
 | 
									userId: user.id,
 | 
				
			||||||
 | 
									ip: request.ip,
 | 
				
			||||||
 | 
									headers: request.headers,
 | 
				
			||||||
 | 
									success: true,
 | 
				
			||||||
 | 
								}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
								// Publish signin event
 | 
				
			||||||
 | 
								this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (redirect) {
 | 
							if (redirect) {
 | 
				
			||||||
			//#region Cookie
 | 
								//#region Cookie
 | 
				
			||||||
			ctx.cookies.set('igi', user.token!, {
 | 
								reply.cookies.set('igi', user.token!, {
 | 
				
			||||||
				path: '/',
 | 
									path: '/',
 | 
				
			||||||
				// SEE: https://github.com/koajs/koa/issues/974
 | 
									// SEE: https://github.com/koajs/koa/issues/974
 | 
				
			||||||
				// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
 | 
									// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
 | 
				
			||||||
| 
						 | 
					@ -36,29 +50,14 @@ export class SigninService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			//#endregion
 | 
								//#endregion
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.redirect(this.config.url);
 | 
								reply.redirect(this.config.url);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ctx.body = {
 | 
								reply.code(200);
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
				id: user.id,
 | 
									id: user.id,
 | 
				
			||||||
				i: user.token,
 | 
									i: user.token,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			ctx.status = 200;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		(async () => {
 | 
					 | 
				
			||||||
			// Append signin history
 | 
					 | 
				
			||||||
			const record = await this.signinsRepository.insert({
 | 
					 | 
				
			||||||
				id: this.idService.genId(),
 | 
					 | 
				
			||||||
				createdAt: new Date(),
 | 
					 | 
				
			||||||
				userId: user.id,
 | 
					 | 
				
			||||||
				ip: ctx.ip,
 | 
					 | 
				
			||||||
				headers: ctx.headers,
 | 
					 | 
				
			||||||
				success: true,
 | 
					 | 
				
			||||||
			}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
			// Publish signin event
 | 
					 | 
				
			||||||
			this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
 | 
					 | 
				
			||||||
		})();
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import rndstr from 'rndstr';
 | 
					import rndstr from 'rndstr';
 | 
				
			||||||
import bcrypt from 'bcryptjs';
 | 
					import bcrypt from 'bcryptjs';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -11,8 +12,8 @@ import { SignupService } from '@/core/SignupService.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
import { EmailService } from '@/core/EmailService.js';
 | 
					import { EmailService } from '@/core/EmailService.js';
 | 
				
			||||||
import { ILocalUser } from '@/models/entities/User.js';
 | 
					import { ILocalUser } from '@/models/entities/User.js';
 | 
				
			||||||
 | 
					import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 | 
				
			||||||
import { SigninService } from './SigninService.js';
 | 
					import { SigninService } from './SigninService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SignupApiService {
 | 
					export class SignupApiService {
 | 
				
			||||||
| 
						 | 
					@ -42,8 +43,22 @@ export class SignupApiService {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async signup(ctx: Koa.Context) {
 | 
						public async signup(
 | 
				
			||||||
		const body = ctx.request.body;
 | 
							request: FastifyRequest<{
 | 
				
			||||||
 | 
								Body: {
 | 
				
			||||||
 | 
									username: string;
 | 
				
			||||||
 | 
									password: string;
 | 
				
			||||||
 | 
									host?: string;
 | 
				
			||||||
 | 
									invitationCode?: string;
 | 
				
			||||||
 | 
									emailAddress?: string;
 | 
				
			||||||
 | 
									'hcaptcha-response'?: string;
 | 
				
			||||||
 | 
									'g-recaptcha-response'?: string;
 | 
				
			||||||
 | 
									'turnstile-response'?: string;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const body = request.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const instance = await this.metaService.fetch(true);
 | 
							const instance = await this.metaService.fetch(true);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -51,20 +66,20 @@ export class SignupApiService {
 | 
				
			||||||
		// ただしテスト時はこの機構は障害となるため無効にする
 | 
							// ただしテスト時はこの機構は障害となるため無効にする
 | 
				
			||||||
		if (process.env.NODE_ENV !== 'test') {
 | 
							if (process.env.NODE_ENV !== 'test') {
 | 
				
			||||||
			if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
 | 
								if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
 | 
				
			||||||
				await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
 | 
									await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
 | 
				
			||||||
					ctx.throw(400, e);
 | 
										throw new FastifyReplyError(400, err);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
 | 
								if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
 | 
				
			||||||
				await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
 | 
									await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
 | 
				
			||||||
					ctx.throw(400, e);
 | 
										throw new FastifyReplyError(400, err);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (instance.enableTurnstile && instance.turnstileSecretKey) {
 | 
								if (instance.enableTurnstile && instance.turnstileSecretKey) {
 | 
				
			||||||
				await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(e => {
 | 
									await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
 | 
				
			||||||
					ctx.throw(400, e);
 | 
										throw new FastifyReplyError(400, err);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -77,20 +92,20 @@ export class SignupApiService {
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (instance.emailRequiredForSignup) {
 | 
							if (instance.emailRequiredForSignup) {
 | 
				
			||||||
			if (emailAddress == null || typeof emailAddress !== 'string') {
 | 
								if (emailAddress == null || typeof emailAddress !== 'string') {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			const available = await this.emailService.validateEmailForAccount(emailAddress);
 | 
								const res = await this.emailService.validateEmailForAccount(emailAddress);
 | 
				
			||||||
			if (!available) {
 | 
								if (!res.available) {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (instance.disableRegistration) {
 | 
							if (instance.disableRegistration) {
 | 
				
			||||||
			if (invitationCode == null || typeof invitationCode !== 'string') {
 | 
								if (invitationCode == null || typeof invitationCode !== 'string') {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -99,7 +114,7 @@ export class SignupApiService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			if (ticket == null) {
 | 
								if (ticket == null) {
 | 
				
			||||||
				ctx.status = 400;
 | 
									reply.code(400);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -117,18 +132,18 @@ export class SignupApiService {
 | 
				
			||||||
				id: this.idService.genId(),
 | 
									id: this.idService.genId(),
 | 
				
			||||||
				createdAt: new Date(),
 | 
									createdAt: new Date(),
 | 
				
			||||||
				code,
 | 
									code,
 | 
				
			||||||
				email: emailAddress,
 | 
									email: emailAddress!,
 | 
				
			||||||
				username: username,
 | 
									username: username,
 | 
				
			||||||
				password: hash,
 | 
									password: hash,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			const link = `${this.config.url}/signup-complete/${code}`;
 | 
								const link = `${this.config.url}/signup-complete/${code}`;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			this.emailService.sendEmail(emailAddress, 'Signup',
 | 
								this.emailService.sendEmail(emailAddress!, 'Signup',
 | 
				
			||||||
				`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
 | 
									`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
 | 
				
			||||||
				`To complete signup, please click this link: ${link}`);
 | 
									`To complete signup, please click this link: ${link}`);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.status = 204;
 | 
								reply.code(204);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const { account, secret } = await this.signupService.signup({
 | 
									const { account, secret } = await this.signupService.signup({
 | 
				
			||||||
| 
						 | 
					@ -140,17 +155,18 @@ export class SignupApiService {
 | 
				
			||||||
					includeSecrets: true,
 | 
										includeSecrets: true,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
				(res as any).token = secret;
 | 
									return {
 | 
				
			||||||
	
 | 
										...res,
 | 
				
			||||||
				ctx.body = res;
 | 
										token: secret,
 | 
				
			||||||
			} catch (e) {
 | 
									};
 | 
				
			||||||
				ctx.throw(400, e);
 | 
								} catch (err) {
 | 
				
			||||||
 | 
									throw new FastifyReplyError(400, err);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async signupPending(ctx: Koa.Context) {
 | 
						public async signupPending(request: FastifyRequest<{ Body: { code: string; } }>, reply: FastifyReply) {
 | 
				
			||||||
		const body = ctx.request.body;
 | 
							const body = request.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const code = body['code'];
 | 
							const code = body['code'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,9 +190,9 @@ export class SignupApiService {
 | 
				
			||||||
				emailVerifyCode: null,
 | 
									emailVerifyCode: null,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.signinService.signin(ctx, account as ILocalUser);
 | 
								this.signinService.signin(request, reply, account as ILocalUser);
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (err) {
 | 
				
			||||||
			ctx.throw(400, e);
 | 
								throw new FastifyReplyError(400, err);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,23 +14,28 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Response = Record<string, any> | void;
 | 
					export type Response = Record<string, any> | void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type File = {
 | 
				
			||||||
 | 
						name: string | null;
 | 
				
			||||||
 | 
						path: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
 | 
					// TODO: paramsの型をT['params']のスキーマ定義から推論する
 | 
				
			||||||
type executor<T extends IEndpointMeta, Ps extends Schema> =
 | 
					type executor<T extends IEndpointMeta, Ps extends Schema> =
 | 
				
			||||||
	(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
 | 
						(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
 | 
				
			||||||
		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
 | 
							Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
 | 
					export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
 | 
				
			||||||
	public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
 | 
						public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
 | 
						constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
 | 
				
			||||||
		const validate = ajv.compile(paramDef);
 | 
							const validate = ajv.compile(paramDef);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
 | 
							this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
 | 
				
			||||||
			let cleanup: undefined | (() => void) = undefined;
 | 
								let cleanup: undefined | (() => void) = undefined;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			if (meta.requireFile) {
 | 
								if (meta.requireFile) {
 | 
				
			||||||
				cleanup = () => {
 | 
									cleanup = () => {
 | 
				
			||||||
					fs.unlink(file.path, () => {});
 | 
										if (file) fs.unlink(file.path, () => {});
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
				if (file == null) return Promise.reject(new ApiError({
 | 
									if (file == null) return Promise.reject(new ApiError({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,8 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
 | 
							super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
 | 
				
			||||||
			// Get 'name' parameter
 | 
								// Get 'name' parameter
 | 
				
			||||||
			let name = ps.name ?? file.originalname;
 | 
								let name = ps.name ?? file!.name ?? null;
 | 
				
			||||||
			if (name !== undefined && name !== null) {
 | 
								if (name != null) {
 | 
				
			||||||
				name = name.trim();
 | 
									name = name.trim();
 | 
				
			||||||
				if (name.length === 0) {
 | 
									if (name.length === 0) {
 | 
				
			||||||
					name = null;
 | 
										name = null;
 | 
				
			||||||
| 
						 | 
					@ -88,8 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				} else if (!this.driveFileEntityService.validateFileName(name)) {
 | 
									} else if (!this.driveFileEntityService.validateFileName(name)) {
 | 
				
			||||||
					throw new ApiError(meta.errors.invalidFileName);
 | 
										throw new ApiError(meta.errors.invalidFileName);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				name = null;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const meta = await this.metaService.fetch();
 | 
								const meta = await this.metaService.fetch();
 | 
				
			||||||
| 
						 | 
					@ -98,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				// Create file
 | 
									// Create file
 | 
				
			||||||
				const driveFile = await this.driveService.addFile({
 | 
									const driveFile = await this.driveService.addFile({
 | 
				
			||||||
					user: me,
 | 
										user: me,
 | 
				
			||||||
					path: file.path,
 | 
										path: file!.path,
 | 
				
			||||||
					name,
 | 
										name,
 | 
				
			||||||
					comment: ps.comment,
 | 
										comment: ps.comment,
 | 
				
			||||||
					folderId: ps.folderId,
 | 
										folderId: ps.folderId,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Redis from 'ioredis';
 | 
					import Redis from 'ioredis';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import { OAuth2 } from 'oauth';
 | 
					import { OAuth2 } from 'oauth';
 | 
				
			||||||
import { v4 as uuid } from 'uuid';
 | 
					import { v4 as uuid } from 'uuid';
 | 
				
			||||||
import { IsNull } from 'typeorm';
 | 
					import { IsNull } from 'typeorm';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
 | 
					import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 | 
				
			||||||
import { SigninService } from '../SigninService.js';
 | 
					import { SigninService } from '../SigninService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class DiscordServerService {
 | 
					export class DiscordServerService {
 | 
				
			||||||
| 
						 | 
					@ -36,21 +36,18 @@ export class DiscordServerService {
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
		private signinService: SigninService,
 | 
							private signinService: SigninService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.create = this.create.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public create() {
 | 
						public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const router = new Router();
 | 
							fastify.get('/disconnect/discord', async (request, reply) => {
 | 
				
			||||||
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
		router.get('/disconnect/discord', async ctx => {
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
					 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneByOrFail({
 | 
								const user = await this.usersRepository.findOneByOrFail({
 | 
				
			||||||
| 
						 | 
					@ -66,13 +63,13 @@ export class DiscordServerService {
 | 
				
			||||||
				integrations: profile.integrations,
 | 
									integrations: profile.integrations,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = 'Discordの連携を解除しました :v:';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Publish i updated event
 | 
								// Publish i updated event
 | 
				
			||||||
			this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
								this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
				detail: true,
 | 
									detail: true,
 | 
				
			||||||
				includeSecrets: true,
 | 
									includeSecrets: true,
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return 'Discordの連携を解除しました :v:';
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const getOAuth2 = async () => {
 | 
							const getOAuth2 = async () => {
 | 
				
			||||||
| 
						 | 
					@ -90,16 +87,14 @@ export class DiscordServerService {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/connect/discord', async ctx => {
 | 
							fastify.get('/connect/discord', async (request, reply) => {
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const params = {
 | 
								const params = {
 | 
				
			||||||
| 
						 | 
					@ -112,10 +107,10 @@ export class DiscordServerService {
 | 
				
			||||||
			this.redisClient.set(userToken, JSON.stringify(params));
 | 
								this.redisClient.set(userToken, JSON.stringify(params));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOAuth2();
 | 
								const oauth2 = await getOAuth2();
 | 
				
			||||||
			ctx.redirect(oauth2!.getAuthorizeUrl(params));
 | 
								reply.redirect(oauth2!.getAuthorizeUrl(params));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/signin/discord', async ctx => {
 | 
							fastify.get('/signin/discord', async (request, reply) => {
 | 
				
			||||||
			const sessid = uuid();
 | 
								const sessid = uuid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const params = {
 | 
								const params = {
 | 
				
			||||||
| 
						 | 
					@ -125,7 +120,7 @@ export class DiscordServerService {
 | 
				
			||||||
				response_type: 'code',
 | 
									response_type: 'code',
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.cookies.set('signin_with_discord_sid', sessid, {
 | 
								reply.cookies.set('signin_with_discord_sid', sessid, {
 | 
				
			||||||
				path: '/',
 | 
									path: '/',
 | 
				
			||||||
				secure: this.config.url.startsWith('https'),
 | 
									secure: this.config.url.startsWith('https'),
 | 
				
			||||||
				httpOnly: true,
 | 
									httpOnly: true,
 | 
				
			||||||
| 
						 | 
					@ -134,27 +129,25 @@ export class DiscordServerService {
 | 
				
			||||||
			this.redisClient.set(sessid, JSON.stringify(params));
 | 
								this.redisClient.set(sessid, JSON.stringify(params));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOAuth2();
 | 
								const oauth2 = await getOAuth2();
 | 
				
			||||||
			ctx.redirect(oauth2!.getAuthorizeUrl(params));
 | 
								reply.redirect(oauth2!.getAuthorizeUrl(params));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/dc/cb', async ctx => {
 | 
							fastify.get('/dc/cb', async (request, reply) => {
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOAuth2();
 | 
								const oauth2 = await getOAuth2();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				const sessid = ctx.cookies.get('signin_with_discord_sid');
 | 
									const sessid = request.cookies.get('signin_with_discord_sid');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!sessid) {
 | 
									if (!sessid) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const code = ctx.query.code;
 | 
									const code = request.query.code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!code || typeof code !== 'string') {
 | 
									if (!code || typeof code !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
									const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -164,9 +157,8 @@ export class DiscordServerService {
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (ctx.query.state !== state) {
 | 
									if (request.query.state !== state) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
 | 
									const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
 | 
				
			||||||
| 
						 | 
					@ -192,8 +184,7 @@ export class DiscordServerService {
 | 
				
			||||||
				})) as Record<string, unknown>;
 | 
									})) as Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
 | 
									if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const profile = await this.userProfilesRepository.createQueryBuilder()
 | 
									const profile = await this.userProfilesRepository.createQueryBuilder()
 | 
				
			||||||
| 
						 | 
					@ -202,8 +193,7 @@ export class DiscordServerService {
 | 
				
			||||||
					.getOne();
 | 
										.getOne();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (profile == null) {
 | 
									if (profile == null) {
 | 
				
			||||||
					ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
 | 
										throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				await this.userProfilesRepository.update(profile.userId, {
 | 
									await this.userProfilesRepository.update(profile.userId, {
 | 
				
			||||||
| 
						 | 
					@ -220,13 +210,12 @@ export class DiscordServerService {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
 | 
									return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				const code = ctx.query.code;
 | 
									const code = request.query.code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!code || typeof code !== 'string') {
 | 
									if (!code || typeof code !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
									const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -236,9 +225,8 @@ export class DiscordServerService {
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (ctx.query.state !== state) {
 | 
									if (request.query.state !== state) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
 | 
									const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
 | 
				
			||||||
| 
						 | 
					@ -263,8 +251,7 @@ export class DiscordServerService {
 | 
				
			||||||
					'Authorization': `Bearer ${accessToken}`,
 | 
										'Authorization': `Bearer ${accessToken}`,
 | 
				
			||||||
				})) as Record<string, unknown>;
 | 
									})) as Record<string, unknown>;
 | 
				
			||||||
				if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
 | 
									if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const user = await this.usersRepository.findOneByOrFail({
 | 
									const user = await this.usersRepository.findOneByOrFail({
 | 
				
			||||||
| 
						 | 
					@ -288,29 +275,29 @@ export class DiscordServerService {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Publish i updated event
 | 
									// Publish i updated event
 | 
				
			||||||
				this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
									this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
					detail: true,
 | 
										detail: true,
 | 
				
			||||||
					includeSecrets: true,
 | 
										includeSecrets: true,
 | 
				
			||||||
				}));
 | 
									}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return router;
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private getUserToken(ctx: Koa.BaseContext): string | null {
 | 
						private getUserToken(request: FastifyRequest): string | null {
 | 
				
			||||||
		return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
							return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	private compareOrigin(ctx: Koa.BaseContext): boolean {
 | 
						private compareOrigin(request: FastifyRequest): boolean {
 | 
				
			||||||
		function normalizeUrl(url?: string): string {
 | 
							function normalizeUrl(url?: string): string {
 | 
				
			||||||
			return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
								return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const referer = ctx.headers['referer'];
 | 
							const referer = request.headers['referer'];
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
							return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Redis from 'ioredis';
 | 
					import Redis from 'ioredis';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import { OAuth2 } from 'oauth';
 | 
					import { OAuth2 } from 'oauth';
 | 
				
			||||||
import { v4 as uuid } from 'uuid';
 | 
					import { v4 as uuid } from 'uuid';
 | 
				
			||||||
import { IsNull } from 'typeorm';
 | 
					import { IsNull } from 'typeorm';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
 | 
					import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 | 
				
			||||||
import { SigninService } from '../SigninService.js';
 | 
					import { SigninService } from '../SigninService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class GithubServerService {
 | 
					export class GithubServerService {
 | 
				
			||||||
| 
						 | 
					@ -36,21 +36,18 @@ export class GithubServerService {
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
		private signinService: SigninService,
 | 
							private signinService: SigninService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.create = this.create.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public create() {
 | 
						public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const router = new Router();
 | 
							fastify.get('/disconnect/github', async (request, reply) => {
 | 
				
			||||||
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
		router.get('/disconnect/github', async ctx => {
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
					 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneByOrFail({
 | 
								const user = await this.usersRepository.findOneByOrFail({
 | 
				
			||||||
| 
						 | 
					@ -66,13 +63,13 @@ export class GithubServerService {
 | 
				
			||||||
				integrations: profile.integrations,
 | 
									integrations: profile.integrations,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = 'GitHubの連携を解除しました :v:';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Publish i updated event
 | 
								// Publish i updated event
 | 
				
			||||||
			this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
								this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
				detail: true,
 | 
									detail: true,
 | 
				
			||||||
				includeSecrets: true,
 | 
									includeSecrets: true,
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return 'GitHubの連携を解除しました :v:';
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const getOath2 = async () => {
 | 
							const getOath2 = async () => {
 | 
				
			||||||
| 
						 | 
					@ -90,16 +87,14 @@ export class GithubServerService {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/connect/github', async ctx => {
 | 
							fastify.get('/connect/github', async (request, reply) => {
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const params = {
 | 
								const params = {
 | 
				
			||||||
| 
						 | 
					@ -111,10 +106,10 @@ export class GithubServerService {
 | 
				
			||||||
			this.redisClient.set(userToken, JSON.stringify(params));
 | 
								this.redisClient.set(userToken, JSON.stringify(params));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOath2();
 | 
								const oauth2 = await getOath2();
 | 
				
			||||||
			ctx.redirect(oauth2!.getAuthorizeUrl(params));
 | 
								reply.redirect(oauth2!.getAuthorizeUrl(params));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/signin/github', async ctx => {
 | 
							fastify.get('/signin/github', async (request, reply) => {
 | 
				
			||||||
			const sessid = uuid();
 | 
								const sessid = uuid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const params = {
 | 
								const params = {
 | 
				
			||||||
| 
						 | 
					@ -123,7 +118,7 @@ export class GithubServerService {
 | 
				
			||||||
				state: uuid(),
 | 
									state: uuid(),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.cookies.set('signin_with_github_sid', sessid, {
 | 
								reply.cookies.set('signin_with_github_sid', sessid, {
 | 
				
			||||||
				path: '/',
 | 
									path: '/',
 | 
				
			||||||
				secure: this.config.url.startsWith('https'),
 | 
									secure: this.config.url.startsWith('https'),
 | 
				
			||||||
				httpOnly: true,
 | 
									httpOnly: true,
 | 
				
			||||||
| 
						 | 
					@ -132,27 +127,25 @@ export class GithubServerService {
 | 
				
			||||||
			this.redisClient.set(sessid, JSON.stringify(params));
 | 
								this.redisClient.set(sessid, JSON.stringify(params));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOath2();
 | 
								const oauth2 = await getOath2();
 | 
				
			||||||
			ctx.redirect(oauth2!.getAuthorizeUrl(params));
 | 
								reply.redirect(oauth2!.getAuthorizeUrl(params));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/gh/cb', async ctx => {
 | 
							fastify.get('/gh/cb', async (request, reply) => {
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oauth2 = await getOath2();
 | 
								const oauth2 = await getOath2();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!userToken) {
 | 
								if (!userToken) {
 | 
				
			||||||
				const sessid = ctx.cookies.get('signin_with_github_sid');
 | 
									const sessid = request.cookies.get('signin_with_github_sid');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!sessid) {
 | 
									if (!sessid) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const code = ctx.query.code;
 | 
									const code = request.query.code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!code || typeof code !== 'string') {
 | 
									if (!code || typeof code !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
									const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -162,9 +155,8 @@ export class GithubServerService {
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (ctx.query.state !== state) {
 | 
									if (request.query.state !== state) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
 | 
									const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
 | 
				
			||||||
| 
						 | 
					@ -184,8 +176,7 @@ export class GithubServerService {
 | 
				
			||||||
					'Authorization': `bearer ${accessToken}`,
 | 
										'Authorization': `bearer ${accessToken}`,
 | 
				
			||||||
				})) as Record<string, unknown>;
 | 
									})) as Record<string, unknown>;
 | 
				
			||||||
				if (typeof login !== 'string' || typeof id !== 'string') {
 | 
									if (typeof login !== 'string' || typeof id !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const link = await this.userProfilesRepository.createQueryBuilder()
 | 
									const link = await this.userProfilesRepository.createQueryBuilder()
 | 
				
			||||||
| 
						 | 
					@ -194,17 +185,15 @@ export class GithubServerService {
 | 
				
			||||||
					.getOne();
 | 
										.getOne();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (link == null) {
 | 
									if (link == null) {
 | 
				
			||||||
					ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
 | 
										throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
 | 
									return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				const code = ctx.query.code;
 | 
									const code = request.query.code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!code || typeof code !== 'string') {
 | 
									if (!code || typeof code !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
									const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -214,9 +203,8 @@ export class GithubServerService {
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (ctx.query.state !== state) {
 | 
									if (request.query.state !== state) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
 | 
									const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
 | 
				
			||||||
| 
						 | 
					@ -238,8 +226,7 @@ export class GithubServerService {
 | 
				
			||||||
				})) as Record<string, unknown>;
 | 
									})) as Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (typeof login !== 'string' || typeof id !== 'string') {
 | 
									if (typeof login !== 'string' || typeof id !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const user = await this.usersRepository.findOneByOrFail({
 | 
									const user = await this.usersRepository.findOneByOrFail({
 | 
				
			||||||
| 
						 | 
					@ -260,29 +247,29 @@ export class GithubServerService {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Publish i updated event
 | 
									// Publish i updated event
 | 
				
			||||||
				this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
									this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
					detail: true,
 | 
										detail: true,
 | 
				
			||||||
					includeSecrets: true,
 | 
										includeSecrets: true,
 | 
				
			||||||
				}));
 | 
									}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return router;
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private getUserToken(ctx: Koa.BaseContext): string | null {
 | 
						private getUserToken(request: FastifyRequest): string | null {
 | 
				
			||||||
		return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
							return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	private compareOrigin(ctx: Koa.BaseContext): boolean {
 | 
						private compareOrigin(request: FastifyRequest): boolean {
 | 
				
			||||||
		function normalizeUrl(url?: string): string {
 | 
							function normalizeUrl(url?: string): string {
 | 
				
			||||||
			return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
								return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const referer = ctx.headers['referer'];
 | 
							const referer = request.headers['referer'];
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
							return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import Redis from 'ioredis';
 | 
					import Redis from 'ioredis';
 | 
				
			||||||
import Router from '@koa/router';
 | 
					import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
 | 
				
			||||||
import { v4 as uuid } from 'uuid';
 | 
					import { v4 as uuid } from 'uuid';
 | 
				
			||||||
import { IsNull } from 'typeorm';
 | 
					import { IsNull } from 'typeorm';
 | 
				
			||||||
import autwh from 'autwh';
 | 
					import autwh from 'autwh';
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
 | 
					import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 | 
				
			||||||
import { SigninService } from '../SigninService.js';
 | 
					import { SigninService } from '../SigninService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class TwitterServerService {
 | 
					export class TwitterServerService {
 | 
				
			||||||
| 
						 | 
					@ -36,21 +36,18 @@ export class TwitterServerService {
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
		private signinService: SigninService,
 | 
							private signinService: SigninService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.create = this.create.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public create() {
 | 
						public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const router = new Router();
 | 
							fastify.get('/disconnect/twitter', async (request, reply) => {
 | 
				
			||||||
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
		router.get('/disconnect/twitter', async ctx => {
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
					 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (userToken == null) {
 | 
								if (userToken == null) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const user = await this.usersRepository.findOneByOrFail({
 | 
								const user = await this.usersRepository.findOneByOrFail({
 | 
				
			||||||
| 
						 | 
					@ -66,13 +63,13 @@ export class TwitterServerService {
 | 
				
			||||||
				integrations: profile.integrations,
 | 
									integrations: profile.integrations,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.body = 'Twitterの連携を解除しました :v:';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Publish i updated event
 | 
								// Publish i updated event
 | 
				
			||||||
			this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
								this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
				detail: true,
 | 
									detail: true,
 | 
				
			||||||
				includeSecrets: true,
 | 
									includeSecrets: true,
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return 'Twitterの連携を解除しました :v:';
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const getTwAuth = async () => {
 | 
							const getTwAuth = async () => {
 | 
				
			||||||
| 
						 | 
					@ -89,25 +86,23 @@ export class TwitterServerService {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/connect/twitter', async ctx => {
 | 
							fastify.get('/connect/twitter', async (request, reply) => {
 | 
				
			||||||
			if (!this.compareOrigin(ctx)) {
 | 
								if (!this.compareOrigin(request)) {
 | 
				
			||||||
				ctx.throw(400, 'invalid origin');
 | 
									throw new FastifyReplyError(400, 'invalid origin');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
			if (userToken == null) {
 | 
								if (userToken == null) {
 | 
				
			||||||
				ctx.throw(400, 'signin required');
 | 
									throw new FastifyReplyError(400, 'signin required');
 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const twAuth = await getTwAuth();
 | 
								const twAuth = await getTwAuth();
 | 
				
			||||||
			const twCtx = await twAuth!.begin();
 | 
								const twCtx = await twAuth!.begin();
 | 
				
			||||||
			this.redisClient.set(userToken, JSON.stringify(twCtx));
 | 
								this.redisClient.set(userToken, JSON.stringify(twCtx));
 | 
				
			||||||
			ctx.redirect(twCtx.url);
 | 
								reply.redirect(twCtx.url);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/signin/twitter', async ctx => {
 | 
							fastify.get('/signin/twitter', async (request, reply) => {
 | 
				
			||||||
			const twAuth = await getTwAuth();
 | 
								const twAuth = await getTwAuth();
 | 
				
			||||||
			const twCtx = await twAuth!.begin();
 | 
								const twCtx = await twAuth!.begin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,26 +110,25 @@ export class TwitterServerService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.redisClient.set(sessid, JSON.stringify(twCtx));
 | 
								this.redisClient.set(sessid, JSON.stringify(twCtx));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.cookies.set('signin_with_twitter_sid', sessid, {
 | 
								reply.cookies.set('signin_with_twitter_sid', sessid, {
 | 
				
			||||||
				path: '/',
 | 
									path: '/',
 | 
				
			||||||
				secure: this.config.url.startsWith('https'),
 | 
									secure: this.config.url.startsWith('https'),
 | 
				
			||||||
				httpOnly: true,
 | 
									httpOnly: true,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.redirect(twCtx.url);
 | 
								reply.redirect(twCtx.url);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/tw/cb', async ctx => {
 | 
							fastify.get('/tw/cb', async (request, reply) => {
 | 
				
			||||||
			const userToken = this.getUserToken(ctx);
 | 
								const userToken = this.getUserToken(request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const twAuth = await getTwAuth();
 | 
								const twAuth = await getTwAuth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (userToken == null) {
 | 
								if (userToken == null) {
 | 
				
			||||||
				const sessid = ctx.cookies.get('signin_with_twitter_sid');
 | 
									const sessid = request.cookies.get('signin_with_twitter_sid');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (sessid == null) {
 | 
									if (sessid == null) {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const get = new Promise<any>((res, rej) => {
 | 
									const get = new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -145,10 +139,9 @@ export class TwitterServerService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const twCtx = await get;
 | 
									const twCtx = await get;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const verifier = ctx.query.oauth_verifier;
 | 
									const verifier = request.query.oauth_verifier;
 | 
				
			||||||
				if (!verifier || typeof verifier !== 'string') {
 | 
									if (!verifier || typeof verifier !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const result = await twAuth!.done(JSON.parse(twCtx), verifier);
 | 
									const result = await twAuth!.done(JSON.parse(twCtx), verifier);
 | 
				
			||||||
| 
						 | 
					@ -159,17 +152,15 @@ export class TwitterServerService {
 | 
				
			||||||
					.getOne();
 | 
										.getOne();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (link == null) {
 | 
									if (link == null) {
 | 
				
			||||||
					ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
 | 
										throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
 | 
									return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				const verifier = ctx.query.oauth_verifier;
 | 
									const verifier = request.query.oauth_verifier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!verifier || typeof verifier !== 'string') {
 | 
									if (!verifier || typeof verifier !== 'string') {
 | 
				
			||||||
					ctx.throw(400, 'invalid session');
 | 
										throw new FastifyReplyError(400, 'invalid session');
 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const get = new Promise<any>((res, rej) => {
 | 
									const get = new Promise<any>((res, rej) => {
 | 
				
			||||||
| 
						 | 
					@ -201,29 +192,29 @@ export class TwitterServerService {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Publish i updated event
 | 
									// Publish i updated event
 | 
				
			||||||
				this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
									this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
 | 
				
			||||||
					detail: true,
 | 
										detail: true,
 | 
				
			||||||
					includeSecrets: true,
 | 
										includeSecrets: true,
 | 
				
			||||||
				}));
 | 
									}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return router;
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private getUserToken(ctx: Koa.BaseContext): string | null {
 | 
						private getUserToken(request: FastifyRequest): string | null {
 | 
				
			||||||
		return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
							return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	private compareOrigin(ctx: Koa.BaseContext): boolean {
 | 
						private compareOrigin(request: FastifyRequest): boolean {
 | 
				
			||||||
		function normalizeUrl(url?: string): string {
 | 
							function normalizeUrl(url?: string): string {
 | 
				
			||||||
			return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
								return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const referer = ctx.headers['referer'];
 | 
							const referer = request.headers['referer'];
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
							return (normalizeUrl(referer) === normalizeUrl(this.config.url));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,16 +3,12 @@ import { fileURLToPath } from 'node:url';
 | 
				
			||||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
 | 
					import { PathOrFileDescriptor, readFileSync } from 'node:fs';
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import ms from 'ms';
 | 
					import ms from 'ms';
 | 
				
			||||||
import Koa from 'koa';
 | 
					 | 
				
			||||||
import Router from '@koa/router';
 | 
					 | 
				
			||||||
import send from 'koa-send';
 | 
					 | 
				
			||||||
import favicon from 'koa-favicon';
 | 
					 | 
				
			||||||
import views from 'koa-views';
 | 
					 | 
				
			||||||
import sharp from 'sharp';
 | 
					import sharp from 'sharp';
 | 
				
			||||||
import { createBullBoard } from '@bull-board/api';
 | 
					import pug from 'pug';
 | 
				
			||||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
 | 
					 | 
				
			||||||
import { KoaAdapter } from '@bull-board/koa';
 | 
					 | 
				
			||||||
import { In, IsNull } from 'typeorm';
 | 
					import { In, IsNull } from 'typeorm';
 | 
				
			||||||
 | 
					import { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
 | 
				
			||||||
 | 
					import fastifyStatic from '@fastify/static';
 | 
				
			||||||
 | 
					import fastifyView from '@fastify/view';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
 | 
					import { getNoteSummary } from '@/misc/get-note-summary.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
| 
						 | 
					@ -84,9 +80,10 @@ export class ClientServerService {
 | 
				
			||||||
		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
							@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
				
			||||||
		@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
 | 
							@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
 | 
							this.createServer = this.createServer.bind(this);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async manifestHandler(ctx: Koa.Context) {
 | 
						private async manifestHandler(reply: FastifyReply) {
 | 
				
			||||||
		const res = deepClone(manifest);
 | 
							const res = deepClone(manifest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const instance = await this.metaService.fetch(true);
 | 
							const instance = await this.metaService.fetch(true);
 | 
				
			||||||
| 
						 | 
					@ -95,27 +92,26 @@ export class ClientServerService {
 | 
				
			||||||
		res.name = instance.name ?? 'Misskey';
 | 
							res.name = instance.name ?? 'Misskey';
 | 
				
			||||||
		if (instance.themeColor) res.theme_color = instance.themeColor;
 | 
							if (instance.themeColor) res.theme_color = instance.themeColor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.set('Cache-Control', 'max-age=300');
 | 
							reply.header('Cache-Control', 'max-age=300');
 | 
				
			||||||
		ctx.body = res;
 | 
							return (res);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createApp() {
 | 
						public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
 | 
				
			||||||
		const app = new Koa();
 | 
							/* TODO
 | 
				
			||||||
 | 
					 | 
				
			||||||
		//#region Bull Dashboard
 | 
							//#region Bull Dashboard
 | 
				
			||||||
		const bullBoardPath = '/queue';
 | 
							const bullBoardPath = '/queue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Authenticate
 | 
							// Authenticate
 | 
				
			||||||
		app.use(async (ctx, next) => {
 | 
							app.use(async (request, reply) => {
 | 
				
			||||||
			if (ctx.path === bullBoardPath || ctx.path.startsWith(bullBoardPath + '/')) {
 | 
								if (ctx.path === bullBoardPath || ctx.path.startsWith(bullBoardPath + '/')) {
 | 
				
			||||||
				const token = ctx.cookies.get('token');
 | 
									const token = ctx.cookies.get('token');
 | 
				
			||||||
				if (token == null) {
 | 
									if (token == null) {
 | 
				
			||||||
					ctx.status = 401;
 | 
										reply.code(401);
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				const user = await this.usersRepository.findOneBy({ token });
 | 
									const user = await this.usersRepository.findOneBy({ token });
 | 
				
			||||||
				if (user == null || !(user.isAdmin || user.isModerator)) {
 | 
									if (user == null || !(user.isAdmin || user.isModerator)) {
 | 
				
			||||||
					ctx.status = 403;
 | 
										reply.code(403);
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -140,83 +136,84 @@ export class ClientServerService {
 | 
				
			||||||
		serverAdapter.setBasePath(bullBoardPath);
 | 
							serverAdapter.setBasePath(bullBoardPath);
 | 
				
			||||||
		app.use(serverAdapter.registerPlugin());
 | 
							app.use(serverAdapter.registerPlugin());
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
							*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init renderer
 | 
							fastify.register(fastifyView, {
 | 
				
			||||||
		app.use(views(_dirname + '/views', {
 | 
								root: _dirname + '/views',
 | 
				
			||||||
			extension: 'pug',
 | 
								engine: {
 | 
				
			||||||
			options: {
 | 
									pug: pug,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								defaultContext: {
 | 
				
			||||||
				version: this.config.version,
 | 
									version: this.config.version,
 | 
				
			||||||
				getClientEntry: () => process.env.NODE_ENV === 'production' ?
 | 
									getClientEntry: () => process.env.NODE_ENV === 'production' ?
 | 
				
			||||||
					this.config.clientEntry :
 | 
										this.config.clientEntry :
 | 
				
			||||||
					JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'],
 | 
										JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'],
 | 
				
			||||||
				config: this.config,
 | 
									config: this.config,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Serve favicon
 | 
					 | 
				
			||||||
		app.use(favicon(`${_dirname}/../../../assets/favicon.ico`));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Common request handler
 | 
					 | 
				
			||||||
		app.use(async (ctx, next) => {
 | 
					 | 
				
			||||||
			// IFrameの中に入れられないようにする
 | 
					 | 
				
			||||||
			ctx.set('X-Frame-Options', 'DENY');
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Init router
 | 
							fastify.addHook('onRequest', (request, reply, done) => {
 | 
				
			||||||
		const router = new Router();
 | 
								// クリックジャッキング防止のためiFrameの中に入れられないようにする
 | 
				
			||||||
 | 
								reply.header('X-Frame-Options', 'DENY');
 | 
				
			||||||
 | 
								done();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region static assets
 | 
							//#region static assets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/static-assets/(.*)', async ctx => {
 | 
							fastify.register(fastifyStatic, {
 | 
				
			||||||
			await send(ctx as any, ctx.path.replace('/static-assets/', ''), {
 | 
								root: _dirname,
 | 
				
			||||||
				root: staticAssets,
 | 
								serve: false,
 | 
				
			||||||
				maxage: ms('7 days'),
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/client-assets/(.*)', async ctx => {
 | 
							fastify.register(fastifyStatic, {
 | 
				
			||||||
			await send(ctx as any, ctx.path.replace('/client-assets/', ''), {
 | 
								root: staticAssets,
 | 
				
			||||||
				root: clientAssets,
 | 
								prefix: '/static-assets/',
 | 
				
			||||||
				maxage: ms('7 days'),
 | 
								maxAge: ms('7 days'),
 | 
				
			||||||
			});
 | 
								decorateReply: false,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/assets/(.*)', async ctx => {
 | 
							fastify.register(fastifyStatic, {
 | 
				
			||||||
			await send(ctx as any, ctx.path.replace('/assets/', ''), {
 | 
								root: clientAssets,
 | 
				
			||||||
				root: assets,
 | 
								prefix: '/client-assets/',
 | 
				
			||||||
				maxage: ms('7 days'),
 | 
								maxAge: ms('7 days'),
 | 
				
			||||||
			});
 | 
								decorateReply: false,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Apple touch icon
 | 
							fastify.register(fastifyStatic, {
 | 
				
			||||||
		router.get('/apple-touch-icon.png', async ctx => {
 | 
								root: assets,
 | 
				
			||||||
			await send(ctx as any, '/apple-touch-icon.png', {
 | 
								prefix: '/assets/',
 | 
				
			||||||
				root: staticAssets,
 | 
								maxAge: ms('7 days'),
 | 
				
			||||||
			});
 | 
								decorateReply: false,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/twemoji/(.*)', async ctx => {
 | 
							fastify.get('/favicon.ico', async (request, reply) => {
 | 
				
			||||||
			const path = ctx.path.replace('/twemoji/', '');
 | 
								return reply.sendFile('/favicon.ico', staticAssets);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fastify.get('/apple-touch-icon.png', async (request, reply) => {
 | 
				
			||||||
 | 
								return reply.sendFile('/apple-touch-icon.png', staticAssets);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fastify.get<{ Params: { path: string } }>('/twemoji/:path(.*)', async (request, reply) => {
 | 
				
			||||||
 | 
								const path = request.params.path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!path.match(/^[0-9a-f-]+\.svg$/)) {
 | 
								if (!path.match(/^[0-9a-f-]+\.svg$/)) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
								reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await send(ctx as any, path, {
 | 
								return await reply.sendFile(path, `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, {
 | 
				
			||||||
				root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`,
 | 
									maxAge: ms('30 days'),
 | 
				
			||||||
				maxage: ms('30 days'),
 | 
					 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/twemoji-badge/(.*)', async ctx => {
 | 
							fastify.get<{ Params: { path: string } }>('/twemoji-badge/:path(.*)', async (request, reply) => {
 | 
				
			||||||
			const path = ctx.path.replace('/twemoji-badge/', '');
 | 
								const path = request.params.path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!path.match(/^[0-9a-f-]+\.png$/)) {
 | 
								if (!path.match(/^[0-9a-f-]+\.png$/)) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -249,44 +246,43 @@ export class ClientServerService {
 | 
				
			||||||
				.png()
 | 
									.png()
 | 
				
			||||||
				.toBuffer();
 | 
									.toBuffer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
								reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=2592000');
 | 
								reply.header('Cache-Control', 'max-age=2592000');
 | 
				
			||||||
			ctx.set('Content-Type', 'image/png');
 | 
								reply.header('Content-Type', 'image/png');
 | 
				
			||||||
			ctx.body = buffer;
 | 
								return buffer;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// ServiceWorker
 | 
							// ServiceWorker
 | 
				
			||||||
		router.get('/sw.js', async ctx => {
 | 
							fastify.get('/sw.js', async (request, reply) => {
 | 
				
			||||||
			await send(ctx as any, '/sw.js', {
 | 
								return await reply.sendFile('/sw.js', swAssets, {
 | 
				
			||||||
				root: swAssets,
 | 
									maxAge: ms('10 minutes'),
 | 
				
			||||||
				maxage: ms('10 minutes'),
 | 
					 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Manifest
 | 
							// Manifest
 | 
				
			||||||
		router.get('/manifest.json', ctx => this.manifestHandler(ctx));
 | 
							fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/robots.txt', async ctx => {
 | 
							fastify.get('/robots.txt', async (request, reply) => {
 | 
				
			||||||
			await send(ctx as any, '/robots.txt', {
 | 
								return await reply.sendFile('/robots.txt', staticAssets);
 | 
				
			||||||
				root: staticAssets,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Docs
 | 
							const renderBase = async (reply: FastifyReply) => {
 | 
				
			||||||
		router.get('/api-doc', async ctx => {
 | 
								const meta = await this.metaService.fetch();
 | 
				
			||||||
			await send(ctx as any, '/redoc.html', {
 | 
								reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
				root: staticAssets,
 | 
								return await reply.view('base', {
 | 
				
			||||||
 | 
									img: meta.bannerUrl,
 | 
				
			||||||
 | 
									title: meta.name ?? 'Misskey',
 | 
				
			||||||
 | 
									instanceName: meta.name ?? 'Misskey',
 | 
				
			||||||
 | 
									desc: meta.description,
 | 
				
			||||||
 | 
									icon: meta.iconUrl,
 | 
				
			||||||
 | 
									themeColor: meta.themeColor,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// URL preview endpoint
 | 
							// URL preview endpoint
 | 
				
			||||||
		router.get('/url', ctx => this.urlPreviewService.handle(ctx));
 | 
							fastify.get<{ Querystring: { url: string; lang: string; } }>('/url', (request, reply) => this.urlPreviewService.handle(request, reply));
 | 
				
			||||||
 | 
					 | 
				
			||||||
		router.get('/api.json', async ctx => {
 | 
					 | 
				
			||||||
			ctx.body = genOpenapiSpec();
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const getFeed = async (acct: string) => {
 | 
							const getFeed = async (acct: string) => {
 | 
				
			||||||
			const { username, host } = Acct.parse(acct);
 | 
								const { username, host } = Acct.parse(acct);
 | 
				
			||||||
| 
						 | 
					@ -300,45 +296,45 @@ export class ClientServerService {
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Atom
 | 
							// Atom
 | 
				
			||||||
		router.get('/@:user.atom', async ctx => {
 | 
							fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
 | 
				
			||||||
			const feed = await getFeed(ctx.params.user);
 | 
								const feed = await getFeed(request.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (feed) {
 | 
								if (feed) {
 | 
				
			||||||
				ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
 | 
									reply.header('Content-Type', 'application/atom+xml; charset=utf-8');
 | 
				
			||||||
				ctx.body = feed.atom1();
 | 
									return feed.atom1();
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// RSS
 | 
							// RSS
 | 
				
			||||||
		router.get('/@:user.rss', async ctx => {
 | 
							fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
 | 
				
			||||||
			const feed = await getFeed(ctx.params.user);
 | 
								const feed = await getFeed(request.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (feed) {
 | 
								if (feed) {
 | 
				
			||||||
				ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
 | 
									reply.header('Content-Type', 'application/rss+xml; charset=utf-8');
 | 
				
			||||||
				ctx.body = feed.rss2();
 | 
									return feed.rss2();
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// JSON
 | 
							// JSON
 | 
				
			||||||
		router.get('/@:user.json', async ctx => {
 | 
							fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
 | 
				
			||||||
			const feed = await getFeed(ctx.params.user);
 | 
								const feed = await getFeed(request.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (feed) {
 | 
								if (feed) {
 | 
				
			||||||
				ctx.set('Content-Type', 'application/json; charset=utf-8');
 | 
									reply.header('Content-Type', 'application/json; charset=utf-8');
 | 
				
			||||||
				ctx.body = feed.json1();
 | 
									return feed.json1();
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region SSR (for crawlers)
 | 
							//#region SSR (for crawlers)
 | 
				
			||||||
		// User
 | 
							// User
 | 
				
			||||||
		router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
 | 
							fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
 | 
				
			||||||
			const { username, host } = Acct.parse(ctx.params.user);
 | 
								const { username, host } = Acct.parse(request.params.user);
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				usernameLower: username.toLowerCase(),
 | 
									usernameLower: username.toLowerCase(),
 | 
				
			||||||
				host: host ?? IsNull(),
 | 
									host: host ?? IsNull(),
 | 
				
			||||||
| 
						 | 
					@ -354,41 +350,41 @@ export class ClientServerService {
 | 
				
			||||||
						.map(field => field.value)
 | 
											.map(field => field.value)
 | 
				
			||||||
					: [];
 | 
										: [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				await ctx.render('user', {
 | 
									reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									return await reply.view('user', {
 | 
				
			||||||
					user, profile, me,
 | 
										user, profile, me,
 | 
				
			||||||
					avatarUrl: await this.userEntityService.getAvatarUrl(user),
 | 
										avatarUrl: await this.userEntityService.getAvatarUrl(user),
 | 
				
			||||||
					sub: ctx.params.sub,
 | 
										sub: request.params.sub,
 | 
				
			||||||
					instanceName: meta.name ?? 'Misskey',
 | 
										instanceName: meta.name ?? 'Misskey',
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=15');
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				// リモートユーザーなので
 | 
									// リモートユーザーなので
 | 
				
			||||||
				// モデレータがAPI経由で参照可能にするために404にはしない
 | 
									// モデレータがAPI経由で参照可能にするために404にはしない
 | 
				
			||||||
				await next();
 | 
									return await renderBase(reply);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/users/:user', async ctx => {
 | 
							fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => {
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.user,
 | 
									id: request.params.user,
 | 
				
			||||||
				host: IsNull(),
 | 
									host: IsNull(),
 | 
				
			||||||
				isSuspended: false,
 | 
									isSuspended: false,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (user == null) {
 | 
								if (user == null) {
 | 
				
			||||||
				ctx.status = 404;
 | 
									reply.code(404);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
 | 
								reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Note
 | 
							// Note
 | 
				
			||||||
		router.get('/notes/:note', async (ctx, next) => {
 | 
							fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
 | 
				
			||||||
			const note = await this.notesRepository.findOneBy({
 | 
								const note = await this.notesRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.note,
 | 
									id: request.params.note,
 | 
				
			||||||
				visibility: In(['public', 'home']),
 | 
									visibility: In(['public', 'home']),
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -396,7 +392,8 @@ export class ClientServerService {
 | 
				
			||||||
				const _note = await this.noteEntityService.pack(note);
 | 
									const _note = await this.noteEntityService.pack(note);
 | 
				
			||||||
				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
 | 
									const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
 | 
				
			||||||
				const meta = await this.metaService.fetch();
 | 
									const meta = await this.metaService.fetch();
 | 
				
			||||||
				await ctx.render('note', {
 | 
									reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									return await reply.view('note', {
 | 
				
			||||||
					note: _note,
 | 
										note: _note,
 | 
				
			||||||
					profile,
 | 
										profile,
 | 
				
			||||||
					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: note.userId })),
 | 
										avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: note.userId })),
 | 
				
			||||||
| 
						 | 
					@ -406,18 +403,14 @@ export class ClientServerService {
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=15');
 | 
									return await renderBase(reply);
 | 
				
			||||||
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Page
 | 
							// Page
 | 
				
			||||||
		router.get('/@:user/pages/:page', async (ctx, next) => {
 | 
							fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
 | 
				
			||||||
			const { username, host } = Acct.parse(ctx.params.user);
 | 
								const { username, host } = Acct.parse(request.params.user);
 | 
				
			||||||
			const user = await this.usersRepository.findOneBy({
 | 
								const user = await this.usersRepository.findOneBy({
 | 
				
			||||||
				usernameLower: username.toLowerCase(),
 | 
									usernameLower: username.toLowerCase(),
 | 
				
			||||||
				host: host ?? IsNull(),
 | 
									host: host ?? IsNull(),
 | 
				
			||||||
| 
						 | 
					@ -426,7 +419,7 @@ export class ClientServerService {
 | 
				
			||||||
			if (user == null) return;
 | 
								if (user == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const page = await this.pagesRepository.findOneBy({
 | 
								const page = await this.pagesRepository.findOneBy({
 | 
				
			||||||
				name: ctx.params.page,
 | 
									name: request.params.page,
 | 
				
			||||||
				userId: user.id,
 | 
									userId: user.id,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -434,7 +427,12 @@ export class ClientServerService {
 | 
				
			||||||
				const _page = await this.pageEntityService.pack(page);
 | 
									const _page = await this.pageEntityService.pack(page);
 | 
				
			||||||
				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
 | 
									const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
 | 
				
			||||||
				const meta = await this.metaService.fetch();
 | 
									const meta = await this.metaService.fetch();
 | 
				
			||||||
				await ctx.render('page', {
 | 
									if (['public'].includes(page.visibility)) {
 | 
				
			||||||
 | 
										reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return await reply.view('page', {
 | 
				
			||||||
					page: _page,
 | 
										page: _page,
 | 
				
			||||||
					profile,
 | 
										profile,
 | 
				
			||||||
					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: page.userId })),
 | 
										avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: page.userId })),
 | 
				
			||||||
| 
						 | 
					@ -442,31 +440,24 @@ export class ClientServerService {
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				if (['public'].includes(page.visibility)) {
 | 
									return await renderBase(reply);
 | 
				
			||||||
					ctx.set('Cache-Control', 'public, max-age=15');
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Clip
 | 
							// Clip
 | 
				
			||||||
		// TODO: 非publicなclipのハンドリング
 | 
							// TODO: 非publicなclipのハンドリング
 | 
				
			||||||
		router.get('/clips/:clip', async (ctx, next) => {
 | 
							fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
 | 
				
			||||||
			const clip = await this.clipsRepository.findOneBy({
 | 
								const clip = await this.clipsRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.clip,
 | 
									id: request.params.clip,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (clip) {
 | 
								if (clip) {
 | 
				
			||||||
				const _clip = await this.clipEntityService.pack(clip);
 | 
									const _clip = await this.clipEntityService.pack(clip);
 | 
				
			||||||
				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
 | 
									const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
 | 
				
			||||||
				const meta = await this.metaService.fetch();
 | 
									const meta = await this.metaService.fetch();
 | 
				
			||||||
				await ctx.render('clip', {
 | 
									reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									return await reply.view('clip', {
 | 
				
			||||||
					clip: _clip,
 | 
										clip: _clip,
 | 
				
			||||||
					profile,
 | 
										profile,
 | 
				
			||||||
					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: clip.userId })),
 | 
										avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: clip.userId })),
 | 
				
			||||||
| 
						 | 
					@ -474,24 +465,21 @@ export class ClientServerService {
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=15');
 | 
									return await renderBase(reply);
 | 
				
			||||||
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Gallery post
 | 
							// Gallery post
 | 
				
			||||||
		router.get('/gallery/:post', async (ctx, next) => {
 | 
							fastify.get<{ Params: { post: string; } }>('/gallery/:post', async (request, reply) => {
 | 
				
			||||||
			const post = await this.galleryPostsRepository.findOneBy({ id: ctx.params.post });
 | 
								const post = await this.galleryPostsRepository.findOneBy({ id: request.params.post });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (post) {
 | 
								if (post) {
 | 
				
			||||||
				const _post = await this.galleryPostEntityService.pack(post);
 | 
									const _post = await this.galleryPostEntityService.pack(post);
 | 
				
			||||||
				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
 | 
									const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
 | 
				
			||||||
				const meta = await this.metaService.fetch();
 | 
									const meta = await this.metaService.fetch();
 | 
				
			||||||
				await ctx.render('gallery-post', {
 | 
									reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									return await reply.view('gallery-post', {
 | 
				
			||||||
					post: _post,
 | 
										post: _post,
 | 
				
			||||||
					profile,
 | 
										profile,
 | 
				
			||||||
					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: post.userId })),
 | 
										avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: post.userId })),
 | 
				
			||||||
| 
						 | 
					@ -499,46 +487,39 @@ export class ClientServerService {
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=15');
 | 
									return await renderBase(reply);
 | 
				
			||||||
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Channel
 | 
							// Channel
 | 
				
			||||||
		router.get('/channels/:channel', async (ctx, next) => {
 | 
							fastify.get<{ Params: { channel: string; } }>('/channels/:channel', async (request, reply) => {
 | 
				
			||||||
			const channel = await this.channelsRepository.findOneBy({
 | 
								const channel = await this.channelsRepository.findOneBy({
 | 
				
			||||||
				id: ctx.params.channel,
 | 
									id: request.params.channel,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (channel) {
 | 
								if (channel) {
 | 
				
			||||||
				const _channel = await this.channelEntityService.pack(channel);
 | 
									const _channel = await this.channelEntityService.pack(channel);
 | 
				
			||||||
				const meta = await this.metaService.fetch();
 | 
									const meta = await this.metaService.fetch();
 | 
				
			||||||
				await ctx.render('channel', {
 | 
									reply.header('Cache-Control', 'public, max-age=15');
 | 
				
			||||||
 | 
									return await reply.view('channel', {
 | 
				
			||||||
					channel: _channel,
 | 
										channel: _channel,
 | 
				
			||||||
					instanceName: meta.name ?? 'Misskey',
 | 
										instanceName: meta.name ?? 'Misskey',
 | 
				
			||||||
					icon: meta.iconUrl,
 | 
										icon: meta.iconUrl,
 | 
				
			||||||
					themeColor: meta.themeColor,
 | 
										themeColor: meta.themeColor,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
				ctx.set('Cache-Control', 'public, max-age=15');
 | 
									return await renderBase(reply);
 | 
				
			||||||
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await next();
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/_info_card_', async ctx => {
 | 
							fastify.get('/_info_card_', async (request, reply) => {
 | 
				
			||||||
			const meta = await this.metaService.fetch(true);
 | 
								const meta = await this.metaService.fetch(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.remove('X-Frame-Options');
 | 
								reply.removeHeader('X-Frame-Options');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await ctx.render('info-card', {
 | 
								return await reply.view('info-card', {
 | 
				
			||||||
				version: this.config.version,
 | 
									version: this.config.version,
 | 
				
			||||||
				host: this.config.host,
 | 
									host: this.config.host,
 | 
				
			||||||
				meta: meta,
 | 
									meta: meta,
 | 
				
			||||||
| 
						 | 
					@ -547,14 +528,14 @@ export class ClientServerService {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/bios', async ctx => {
 | 
							fastify.get('/bios', async (request, reply) => {
 | 
				
			||||||
			await ctx.render('bios', {
 | 
								return await reply.view('bios', {
 | 
				
			||||||
				version: this.config.version,
 | 
									version: this.config.version,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/cli', async ctx => {
 | 
							fastify.get('/cli', async (request, reply) => {
 | 
				
			||||||
			await ctx.render('cli', {
 | 
								return await reply.view('cli', {
 | 
				
			||||||
				version: this.config.version,
 | 
									version: this.config.version,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					@ -562,33 +543,21 @@ export class ClientServerService {
 | 
				
			||||||
		const override = (source: string, target: string, depth = 0) =>
 | 
							const override = (source: string, target: string, depth = 0) =>
 | 
				
			||||||
			[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
 | 
								[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		router.get('/flush', async ctx => {
 | 
							fastify.get('/flush', async (request, reply) => {
 | 
				
			||||||
			await ctx.render('flush');
 | 
								return await reply.view('flush');
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
 | 
							// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
 | 
				
			||||||
		router.get('/streaming', async ctx => {
 | 
							fastify.get('/streaming', async (request, reply) => {
 | 
				
			||||||
			ctx.status = 503;
 | 
								reply.code(503);
 | 
				
			||||||
			ctx.set('Cache-Control', 'private, max-age=0');
 | 
								reply.header('Cache-Control', 'private, max-age=0');
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Render base html for all requests
 | 
							// Render base html for all requests
 | 
				
			||||||
		router.get('(.*)', async ctx => {
 | 
							fastify.get('*', async (request, reply) => {
 | 
				
			||||||
			const meta = await this.metaService.fetch();
 | 
								return await renderBase(reply);
 | 
				
			||||||
			await ctx.render('base', {
 | 
					 | 
				
			||||||
				img: meta.bannerUrl,
 | 
					 | 
				
			||||||
				title: meta.name ?? 'Misskey',
 | 
					 | 
				
			||||||
				instanceName: meta.name ?? 'Misskey',
 | 
					 | 
				
			||||||
				desc: meta.description,
 | 
					 | 
				
			||||||
				icon: meta.iconUrl,
 | 
					 | 
				
			||||||
				themeColor: meta.themeColor,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			ctx.set('Cache-Control', 'public, max-age=15');
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Register router
 | 
							done();
 | 
				
			||||||
		app.use(router.routes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return app;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import summaly from 'summaly';
 | 
					import summaly from 'summaly';
 | 
				
			||||||
 | 
					import { FastifyRequest, FastifyReply } from 'fastify';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { UsersRepository } from '@/models/index.js';
 | 
					import type { UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
| 
						 | 
					@ -8,7 +9,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
 | 
				
			||||||
import type Logger from '@/logger.js';
 | 
					import type Logger from '@/logger.js';
 | 
				
			||||||
import { query } from '@/misc/prelude/url.js';
 | 
					import { query } from '@/misc/prelude/url.js';
 | 
				
			||||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
					import { LoggerService } from '@/core/LoggerService.js';
 | 
				
			||||||
import type Koa from 'koa';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class UrlPreviewService {
 | 
					export class UrlPreviewService {
 | 
				
			||||||
| 
						 | 
					@ -39,16 +39,19 @@ export class UrlPreviewService {
 | 
				
			||||||
			: null;
 | 
								: null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async handle(ctx: Koa.Context) {
 | 
						public async handle(
 | 
				
			||||||
		const url = ctx.query.url;
 | 
							request: FastifyRequest<{ Querystring: { url: string; lang: string; } }>,
 | 
				
			||||||
 | 
							reply: FastifyReply,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							const url = request.query.url;
 | 
				
			||||||
		if (typeof url !== 'string') {
 | 
							if (typeof url !== 'string') {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		const lang = ctx.query.lang;
 | 
							const lang = request.query.lang;
 | 
				
			||||||
		if (Array.isArray(lang)) {
 | 
							if (Array.isArray(lang)) {
 | 
				
			||||||
			ctx.status = 400;
 | 
								reply.code(400);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
| 
						 | 
					@ -73,14 +76,14 @@ export class UrlPreviewService {
 | 
				
			||||||
			summary.thumbnail = this.wrap(summary.thumbnail);
 | 
								summary.thumbnail = this.wrap(summary.thumbnail);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			// Cache 7days
 | 
								// Cache 7days
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=604800, immutable');
 | 
								reply.header('Cache-Control', 'max-age=604800, immutable');
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			ctx.body = summary;
 | 
								return summary;
 | 
				
			||||||
		} catch (err) {
 | 
							} catch (err) {
 | 
				
			||||||
			this.logger.warn(`Failed to get preview of ${url}: ${err}`);
 | 
								this.logger.warn(`Failed to get preview of ${url}: ${err}`);
 | 
				
			||||||
			ctx.status = 200;
 | 
								reply.code(200);
 | 
				
			||||||
			ctx.set('Cache-Control', 'max-age=86400, immutable');
 | 
								reply.header('Cache-Control', 'max-age=86400, immutable');
 | 
				
			||||||
			ctx.body = '{}';
 | 
								return {};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ window.onload = async () => {
 | 
				
			||||||
			if (i) data.i = i;
 | 
								if (i) data.i = i;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			// Send request
 | 
								// Send request
 | 
				
			||||||
			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
 | 
								window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
 | 
				
			||||||
				method: 'POST',
 | 
									method: 'POST',
 | 
				
			||||||
				body: JSON.stringify(data),
 | 
									body: JSON.stringify(data),
 | 
				
			||||||
				credentials: 'omit',
 | 
									credentials: 'omit',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,7 @@
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const res = await fetch(`/assets/locales/${lang}.${v}.json`);
 | 
							const res = await window.fetch(`/assets/locales/${lang}.${v}.json`);
 | 
				
			||||||
		if (res.status === 200) {
 | 
							if (res.status === 200) {
 | 
				
			||||||
			localStorage.setItem('lang', lang);
 | 
								localStorage.setItem('lang', lang);
 | 
				
			||||||
			localStorage.setItem('locale', await res.text());
 | 
								localStorage.setItem('locale', await res.text());
 | 
				
			||||||
| 
						 | 
					@ -290,9 +290,13 @@
 | 
				
			||||||
	// eslint-disable-next-line no-inner-declarations
 | 
						// eslint-disable-next-line no-inner-declarations
 | 
				
			||||||
	async function checkUpdate() {
 | 
						async function checkUpdate() {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			const res = await fetch('/api/meta', {
 | 
								const res = await window.fetch('/api/meta', {
 | 
				
			||||||
				method: 'POST',
 | 
									method: 'POST',
 | 
				
			||||||
				cache: 'no-cache'
 | 
									cache: 'no-cache',
 | 
				
			||||||
 | 
									body: '{}',
 | 
				
			||||||
 | 
									headers: {
 | 
				
			||||||
 | 
										'Content-Type': 'application/json',
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const meta = await res.json();
 | 
								const meta = await res.json();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,12 +33,15 @@ export async function signout() {
 | 
				
			||||||
			const registration = await navigator.serviceWorker.ready;
 | 
								const registration = await navigator.serviceWorker.ready;
 | 
				
			||||||
			const push = await registration.pushManager.getSubscription();
 | 
								const push = await registration.pushManager.getSubscription();
 | 
				
			||||||
			if (push) {
 | 
								if (push) {
 | 
				
			||||||
				await fetch(`${apiUrl}/sw/unregister`, {
 | 
									await window.fetch(`${apiUrl}/sw/unregister`, {
 | 
				
			||||||
					method: 'POST',
 | 
										method: 'POST',
 | 
				
			||||||
					body: JSON.stringify({
 | 
										body: JSON.stringify({
 | 
				
			||||||
						i: $i.token,
 | 
											i: $i.token,
 | 
				
			||||||
						endpoint: push.endpoint,
 | 
											endpoint: push.endpoint,
 | 
				
			||||||
					}),
 | 
										}),
 | 
				
			||||||
 | 
										headers: {
 | 
				
			||||||
 | 
											'Content-Type': 'application/json',
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -80,32 +83,35 @@ export async function removeAccount(id: Account['id']) {
 | 
				
			||||||
function fetchAccount(token: string): Promise<Account> {
 | 
					function fetchAccount(token: string): Promise<Account> {
 | 
				
			||||||
	return new Promise((done, fail) => {
 | 
						return new Promise((done, fail) => {
 | 
				
			||||||
		// Fetch user
 | 
							// Fetch user
 | 
				
			||||||
		fetch(`${apiUrl}/i`, {
 | 
							window.fetch(`${apiUrl}/i`, {
 | 
				
			||||||
			method: 'POST',
 | 
								method: 'POST',
 | 
				
			||||||
			body: JSON.stringify({
 | 
								body: JSON.stringify({
 | 
				
			||||||
				i: token,
 | 
									i: token,
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
 | 
								headers: {
 | 
				
			||||||
 | 
									'Content-Type': 'application/json',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.then(res => res.json())
 | 
								.then(res => res.json())
 | 
				
			||||||
		.then(res => {
 | 
								.then(res => {
 | 
				
			||||||
			if (res.error) {
 | 
									if (res.error) {
 | 
				
			||||||
				if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
 | 
										if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
 | 
				
			||||||
					showSuspendedDialog().then(() => {
 | 
											showSuspendedDialog().then(() => {
 | 
				
			||||||
						signout();
 | 
												signout();
 | 
				
			||||||
					});
 | 
											});
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											alert({
 | 
				
			||||||
 | 
												type: 'error',
 | 
				
			||||||
 | 
												title: i18n.ts.failedToFetchAccountInformation,
 | 
				
			||||||
 | 
												text: JSON.stringify(res.error),
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					alert({
 | 
										res.token = token;
 | 
				
			||||||
						type: 'error',
 | 
										done(res);
 | 
				
			||||||
						title: i18n.ts.failedToFetchAccountInformation,
 | 
					 | 
				
			||||||
						text: JSON.stringify(res.error),
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								})
 | 
				
			||||||
				res.token = token;
 | 
								.catch(fail);
 | 
				
			||||||
				done(res);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		.catch(fail);
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,7 @@ const ok = async () => {
 | 
				
			||||||
				formData.append('folderId', defaultStore.state.uploadFolder);
 | 
									formData.append('folderId', defaultStore.state.uploadFolder);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			fetch(apiUrl + '/drive/files/create', {
 | 
								window.fetch(apiUrl + '/drive/files/create', {
 | 
				
			||||||
				method: 'POST',
 | 
									method: 'POST',
 | 
				
			||||||
				body: formData,
 | 
									body: formData,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,7 @@ let player = $ref({
 | 
				
			||||||
let playerEnabled = $ref(false);
 | 
					let playerEnabled = $ref(false);
 | 
				
			||||||
let tweetId = $ref<string | null>(null);
 | 
					let tweetId = $ref<string | null>(null);
 | 
				
			||||||
let tweetExpanded = $ref(props.detail);
 | 
					let tweetExpanded = $ref(props.detail);
 | 
				
			||||||
const embedId = `embed${Math.random().toString().replace(/\D/,'')}`;
 | 
					const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
 | 
				
			||||||
let tweetHeight = $ref(150);
 | 
					let tweetHeight = $ref(150);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const requestUrl = new URL(props.url);
 | 
					const requestUrl = new URL(props.url);
 | 
				
			||||||
| 
						 | 
					@ -86,7 +86,7 @@ const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
requestUrl.hash = '';
 | 
					requestUrl.hash = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
					window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
				
			||||||
	res.json().then(info => {
 | 
						res.json().then(info => {
 | 
				
			||||||
		if (info.url == null) return;
 | 
							if (info.url == null) return;
 | 
				
			||||||
		title = info.title;
 | 
							title = info.title;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@ const requestLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ytFetch = (): void => {
 | 
					const ytFetch = (): void => {
 | 
				
			||||||
	fetching = true;
 | 
						fetching = true;
 | 
				
			||||||
	fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
						window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
				
			||||||
		res.json().then(info => {
 | 
							res.json().then(info => {
 | 
				
			||||||
			if (info.url == null) return;
 | 
								if (info.url == null) return;
 | 
				
			||||||
			title = info.title;
 | 
								title = info.title;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,12 +25,12 @@ export default defineComponent({
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
		block: {
 | 
							block: {
 | 
				
			||||||
			type: Object as PropType<PostBlock>,
 | 
								type: Object as PropType<PostBlock>,
 | 
				
			||||||
			required: true
 | 
								required: true,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		hpml: {
 | 
							hpml: {
 | 
				
			||||||
			type: Object as PropType<Hpml>,
 | 
								type: Object as PropType<Hpml>,
 | 
				
			||||||
			required: true
 | 
								required: true,
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
| 
						 | 
					@ -44,8 +44,8 @@ export default defineComponent({
 | 
				
			||||||
			handler() {
 | 
								handler() {
 | 
				
			||||||
				this.text = this.hpml.interpolate(this.block.text);
 | 
									this.text = this.hpml.interpolate(this.block.text);
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			deep: true
 | 
								deep: true,
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		upload() {
 | 
							upload() {
 | 
				
			||||||
| 
						 | 
					@ -59,14 +59,14 @@ export default defineComponent({
 | 
				
			||||||
						formData.append('folderId', this.$store.state.uploadFolder);
 | 
											formData.append('folderId', this.$store.state.uploadFolder);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					fetch(apiUrl + '/drive/files/create', {
 | 
										window.fetch(apiUrl + '/drive/files/create', {
 | 
				
			||||||
						method: 'POST',
 | 
											method: 'POST',
 | 
				
			||||||
						body: formData,
 | 
											body: formData,
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
					.then(response => response.json())
 | 
											.then(response => response.json())
 | 
				
			||||||
					.then(f => {
 | 
											.then(f => {
 | 
				
			||||||
						ok(f);
 | 
												ok(f);
 | 
				
			||||||
					});
 | 
											});
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			os.promiseDialog(promise);
 | 
								os.promiseDialog(promise);
 | 
				
			||||||
| 
						 | 
					@ -81,8 +81,8 @@ export default defineComponent({
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				this.posted = true;
 | 
									this.posted = true;
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	}
 | 
						},
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,11 +29,14 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
 | 
				
			||||||
		if (token !== undefined) (data as any).i = token;
 | 
							if (token !== undefined) (data as any).i = token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Send request
 | 
							// Send request
 | 
				
			||||||
		fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
 | 
							window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
 | 
				
			||||||
			method: 'POST',
 | 
								method: 'POST',
 | 
				
			||||||
			body: JSON.stringify(data),
 | 
								body: JSON.stringify(data),
 | 
				
			||||||
			credentials: 'omit',
 | 
								credentials: 'omit',
 | 
				
			||||||
			cache: 'no-cache',
 | 
								cache: 'no-cache',
 | 
				
			||||||
 | 
								headers: {
 | 
				
			||||||
 | 
									'Content-Type': 'application/json',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		}).then(async (res) => {
 | 
							}).then(async (res) => {
 | 
				
			||||||
			const body = res.status === 204 ? null : await res.json();
 | 
								const body = res.status === 204 ? null : await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +66,7 @@ export const apiGet = ((endpoint: string, data: Record<string, any> = {}) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const promise = new Promise((resolve, reject) => {
 | 
						const promise = new Promise((resolve, reject) => {
 | 
				
			||||||
		// Send request
 | 
							// Send request
 | 
				
			||||||
		fetch(`${apiUrl}/${endpoint}?${query}`, {
 | 
							window.fetch(`${apiUrl}/${endpoint}?${query}`, {
 | 
				
			||||||
			method: 'GET',
 | 
								method: 'GET',
 | 
				
			||||||
			credentials: 'omit',
 | 
								credentials: 'omit',
 | 
				
			||||||
			cache: 'default',
 | 
								cache: 'default',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ const fetching = ref(true);
 | 
				
			||||||
let key = $ref(0);
 | 
					let key = $ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tick = () => {
 | 
					const tick = () => {
 | 
				
			||||||
	fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
 | 
						window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
 | 
				
			||||||
		res.json().then(feed => {
 | 
							res.json().then(feed => {
 | 
				
			||||||
			if (props.shuffle) {
 | 
								if (props.shuffle) {
 | 
				
			||||||
				shuffle(feed.items);
 | 
									shuffle(feed.items);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@ const fetching = ref(true);
 | 
				
			||||||
let key = $ref(0);
 | 
					let key = $ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tick = () => {
 | 
					const tick = () => {
 | 
				
			||||||
	fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 | 
						window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 | 
				
			||||||
		res.json().then(feed => {
 | 
							res.json().then(feed => {
 | 
				
			||||||
			if (widgetProps.shuffle) {
 | 
								if (widgetProps.shuffle) {
 | 
				
			||||||
				shuffle(feed.items);
 | 
									shuffle(feed.items);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ const items = ref([]);
 | 
				
			||||||
const fetching = ref(true);
 | 
					const fetching = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tick = () => {
 | 
					const tick = () => {
 | 
				
			||||||
	fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 | 
						window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
 | 
				
			||||||
		res.json().then(feed => {
 | 
							res.json().then(feed => {
 | 
				
			||||||
			items.value = feed.items;
 | 
								items.value = feed.items;
 | 
				
			||||||
			fetching.value = false;
 | 
								fetching.value = false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue