Merge remote-tracking branch 'misskey/master' into feature/2024.9.0

This commit is contained in:
dakkar 2024-10-09 15:17:22 +01:00
commit f00576bce6
564 changed files with 19993 additions and 8169 deletions

View file

@ -80,7 +80,7 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
});
done();
});
@ -145,12 +145,12 @@ export class FileServerService {
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
return await reply.redirect(301, externalThumbnail);
return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@ -165,7 +165,7 @@ export class FileServerService {
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
return await reply.redirect(url.toString(), 301);
}
}
@ -312,8 +312,8 @@ export class FileServerService {
}
return await reply.redirect(
301,
url.toString(),
301,
);
}

View file

@ -27,6 +27,9 @@ export class HealthServerService {
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.redisForReactions)
private redisForReactions: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@ -43,6 +46,7 @@ export class HealthServerService {
this.redisForPub.ping(),
this.redisForSub.ping(),
this.redisForTimelines.ping(),
this.redisForReactions.ping(),
this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503));

View file

@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
imports: [
@ -74,6 +75,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
AuthenticateService,
RateLimiterService,
SigninApiService,
SigninWithPasskeyApiService,
SigninService,
SignupApiService,
StreamingApiServerService,

View file

@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js';
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@ -45,6 +44,9 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -54,7 +56,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private metaService: MetaService,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
@ -167,8 +168,8 @@ export class ServerService implements OnApplicationShutdown {
}
return await reply.redirect(
301,
url.toString(),
301,
);
});
@ -195,7 +196,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
if (this.meta.enableIdenticonGeneration) {
return await genIdenticon(request.params.x);
} else {
return reply.redirect('/static-assets/avatar.png');

View file

@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import type Logger from '@/logger.js';
import type { UserIpsRepository } from '@/models/_.js';
import { MetaService } from '@/core/MetaService.js';
import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.config)
private config: Config,
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
private metaService: MetaService,
private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService,
private roleService: RoleService,
@ -199,9 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
const [path] = await createTemp();
const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartData.file.truncated) {
cleanup();
reply.code(413);
reply.send();
return;
}
const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@ -256,9 +266,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
@bindThis
private async logIp(request: FastifyRequest, user: MiLocalUser) {
const meta = await this.metaService.fetch();
if (!meta.enableIpLogging) return;
private logIp(request: FastifyRequest, user: MiLocalUser) {
if (!this.meta.enableIpLogging) return;
const ip = request.ip;
if (!ip) {
this.logger.warn(`user ${user.id} has a null IP address; please check your network configuration.`);

View file

@ -8,6 +8,7 @@ import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import { ModuleRef } from '@nestjs/core';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
@ -17,6 +18,7 @@ import endpoints from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js';
import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable()
@ -37,6 +39,7 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
private signinWithPasskeyApiService: SigninWithPasskeyApiService,
) {
//this.createServer = this.createServer.bind(this);
}
@ -49,7 +52,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
fileSize: this.config.maxFileSize ?? 262144000,
fileSize: this.config.maxFileSize,
files: 1,
},
});
@ -131,6 +134,12 @@ export class ApiServerService {
};
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{
Body: {
credential?: AuthenticationResponseJSON;
};
}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.get('/v1/instance/peers', async (request, reply) => {

View file

@ -97,6 +97,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@ -266,6 +267,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@ -494,6 +496,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@ -663,6 +666,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
@ -895,6 +899,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@ -1064,6 +1069,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@ -1290,6 +1296,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@ -1459,6 +1466,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,

View file

@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { randomUUID } from 'crypto';
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type {
SigninsRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class SigninWithPasskeyApiService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
private idService: IdService,
private rateLimiterService: RateLimiterService,
private signinService: SigninService,
private webAuthnService: WebAuthnService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('PasskeyAuth');
}
@bindThis
public async signin(
request: FastifyRequest<{
Body: {
credential?: AuthenticationResponseJSON;
context?: string;
};
}>,
reply: FastifyReply,
) {
reply.header('Access-Control-Allow-Origin', this.config.url);
reply.header('Access-Control-Allow-Credentials', 'true');
const body = request.body;
const credential = body['credential'];
function error(status: number, error: { id: string }) {
reply.code(status);
return { error };
}
const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
userId: userId,
ip: request.ip,
headers: request.headers as any,
success: false,
});
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};
try {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
} catch (err) {
reply.code(429);
return {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
}
// Initiate Passkey Auth challenge with context
if (!credential) {
const context = randomUUID();
this.logger.info(`Initiate Passkey challenge: context: ${context}`);
const authChallengeOptions = {
option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context),
context: context,
};
reply.code(200);
return authChallengeOptions;
}
const context = body.context;
if (!context || typeof context !== 'string') {
// If try Authentication without context
return error(400, {
id: '1658cc2e-4495-461f-aee4-d403cdf073c1',
});
}
this.logger.debug(`Try Sign-in with Passkey: context: ${context}`);
let authorizedUserId: MiUser['id'] | null;
try {
authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
} catch (err) {
this.logger.warn(`Passkey challenge Verify error! : ${err}`);
const errorId = (err as IdentifiableError).id;
return error(403, {
id: errorId,
});
}
if (!authorizedUserId) {
return error(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
}
// Fetch user
const user = await this.usersRepository.findOneBy({
id: authorizedUserId,
host: IsNull(),
}) as MiLocalUser | null;
if (user == null) {
return error(403, {
id: '652f899f-66d4-490e-993e-6606c8ec04c3',
});
}
if (user.isSuspended) {
return error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
// Authentication was successful, but passwordless login is not enabled
if (!profile.usePasswordLessLogin) {
return await fail(user.id, 403, {
id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912',
});
}
const signinResponse = this.signinService.signin(request, reply, user);
return {
signinResponse: signinResponse,
};
}
}

View file

@ -8,9 +8,8 @@ import { Inject, Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { IdService } from '@/core/IdService.js';
import { SignupService } from '@/core/SignupService.js';
@ -31,6 +30,9 @@ export class SignupApiService {
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -48,7 +50,6 @@ export class SignupApiService {
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
private captchaService: CaptchaService,
private signupService: SignupService,
private signinService: SigninService,
@ -77,31 +78,29 @@ export class SignupApiService {
) {
const body = request.body;
const instance = await this.metaService.fetch(true);
// Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') {
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (instance.enableTurnstile && instance.turnstileSecretKey) {
await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
@ -114,7 +113,7 @@ export class SignupApiService {
const reason = body['reason'];
const emailAddress = body['emailAddress'];
if (instance.emailRequiredForSignup) {
if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400);
return;
@ -136,7 +135,7 @@ export class SignupApiService {
let ticket: MiRegistrationTicket | null = null;
if (instance.disableRegistration) {
if (this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
@ -157,7 +156,7 @@ export class SignupApiService {
}
// メアド認証が有効の場合
if (instance.emailRequiredForSignup) {
if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー
if (ticket.usedBy) {
reply.code(400);
@ -175,7 +174,7 @@ export class SignupApiService {
}
}
if (instance.emailRequiredForSignup) {
if (this.meta.emailRequiredForSignup) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
@ -185,7 +184,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'USED_USERNAME');
}
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new FastifyReplyError(400, 'DENIED_USERNAME');
}

View file

@ -103,6 +103,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@ -272,6 +273,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@ -498,6 +500,7 @@ const eps = [
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@ -667,6 +670,7 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],

View file

@ -400,6 +400,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
enableReactionsBuffering: {
type: 'boolean',
optional: false, nullable: false,
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
@ -534,6 +538,18 @@ export const meta = {
optional: false, nullable: false,
},
},
federation: {
type: 'string',
optional: false, nullable: false,
},
federationHosts: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
} as const;
@ -669,6 +685,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
enableReactionsBuffering: instance.enableReactionsBuffering,
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
@ -678,6 +695,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
urlPreviewUserAgent: instance.urlPreviewUserAgent,
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
federation: instance.federation,
federationHosts: instance.federationHosts,
};
});
}

View file

@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
],
},
prefixItems: [
{
type: 'string',
},
{
type: 'number',
},
],
unevaluatedItems: false,
},
example: [[
'example.com',

View file

@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
],
},
prefixItems: [
{
type: 'string',
},
{
type: 'number',
},
],
unevaluatedItems: false,
},
example: [[
'example.com',

View file

@ -36,6 +36,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
followedMessage: {
type: 'string',
optional: false, nullable: true,
},
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
@ -237,6 +241,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
emailVerified: profile.emailVerified,
approved: user.approved,
signupReason: user.signupReason,
followedMessage: profile.followedMessage,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,

View file

@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
import { ApiError } from '@/server/api/error.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:system-webhook',
limit: {
duration: ms('15min'),
max: 60,
},
errors: {
noSuchWebhook: {
message: 'No such webhook.',
code: 'NO_SUCH_WEBHOOK',
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
webhookId: {
type: 'string',
format: 'misskey:id',
},
type: {
type: 'string',
enum: systemWebhookEventTypes,
},
override: {
type: 'object',
properties: {
url: { type: 'string', nullable: false },
secret: { type: 'string', nullable: false },
},
},
},
required: ['webhookId', 'type'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private webhookTestService: WebhookTestService,
) {
super(meta, paramDef, async (ps) => {
try {
await this.webhookTestService.testSystemWebhook({
webhookId: ps.webhookId,
type: ps.type,
override: ps.override,
});
} catch (e) {
if (e instanceof WebhookTestService.NoSuchWebhookError) {
throw new ApiError(meta.errors.noSuchWebhook);
}
throw e;
}
});
}
}

View file

@ -151,6 +151,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
enableReactionsBuffering: { type: 'boolean' },
notesPerOneAd: { type: 'integer' },
silencedHosts: {
type: 'array',
@ -181,6 +182,16 @@ export const paramDef = {
type: 'string',
},
},
federation: {
type: 'string',
enum: ['all', 'none', 'specified'],
},
federationHosts: {
type: 'array',
items: {
type: 'string',
},
},
},
required: [],
} as const;
@ -636,6 +647,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
}
if (ps.enableReactionsBuffering !== undefined) {
set.enableReactionsBuffering = ps.enableReactionsBuffering;
}
if (ps.notesPerOneAd !== undefined) {
set.notesPerOneAd = ps.notesPerOneAd;
}
@ -674,6 +689,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean);
}
if (ps.federation !== undefined) {
set.federation = ps.federation;
}
if (Array.isArray(ps.federationHosts)) {
set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);

View file

@ -34,6 +34,12 @@ export const meta = {
code: 'TOO_MANY_ANTENNAS',
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
},
emptyKeyword: {
message: 'Either keywords or excludeKeywords is required.',
code: 'EMPTY_KEYWORD',
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
},
},
res: {
@ -87,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({

View file

@ -32,6 +32,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
emptyKeyword: {
message: 'Either keywords or excludeKeywords is required.',
code: 'EMPTY_KEYWORD',
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
},
},
res: {
@ -85,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
throw new ApiError(meta.errors.emptyKeyword);
}
}
// Fetch the antenna

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -91,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private metaService: MetaService,
private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
@ -112,10 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const host = this.utilityService.extractDbHost(uri);
const fetchedMeta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null;
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@ -123,6 +118,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
const host = this.utilityService.extractDbHost(uri);
// local object, not found in db? fail
if (this.utilityService.isSelfHost(host)) return null;

View file

@ -5,14 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
@ -65,6 +63,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -75,16 +76,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const serverSettings = await this.metaService.fetch();
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
@ -95,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me);
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
}

View file

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { RoleService } from '@/core/RoleService.js';
@ -41,14 +40,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private metaService: MetaService,
private driveFileEntityService: DriveFileEntityService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
// Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
const policies = await this.roleService.getUserPolicies(me.id);

View file

@ -10,6 +10,7 @@ import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['drive', 'notes'],
@ -61,12 +62,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch file
const file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
userId: me.id,
userId: await this.roleService.isModerator(me) ? undefined : me.id,
});
if (file == null) {

View file

@ -4,14 +4,15 @@
*/
import ms from 'ms';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['drive'],
@ -73,8 +74,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private driveFileEntityService: DriveFileEntityService,
private metaService: MetaService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
@ -91,8 +94,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const instance = await this.metaService.fetch();
try {
// Create file
const driveFile = await this.driveService.addFile({
@ -103,8 +104,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
requestIp: instance.enableIpLogging ? ip : null,
requestHeaders: instance.enableIpLogging ? headers : null,
requestIp: this.serverSettings.enableIpLogging ? ip : null,
requestHeaders: this.serverSettings.enableIpLogging ? headers : null,
});
return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) {

View file

@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canImportAntennas',
prohibitMoved: true,
limit: {

View file

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canImportBlocking',
prohibitMoved: true,
limit: {

View file

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canImportFollowing',
prohibitMoved: true,
limit: {
duration: ms('1hour'),

View file

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canImportMuting',
prohibitMoved: true,
limit: {

View file

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canImportUserLists',
prohibitMoved: true,
limit: {
duration: ms('1hour'),

View file

@ -8,7 +8,7 @@ import ms from 'ms';
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmailService } from '@/core/EmailService.js';
import type { Config } from '@/config.js';
@ -16,7 +16,6 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -71,10 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private metaService: MetaService,
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!res.available) {
throw new ApiError(meta.errors.unavailable);
}
} else if ((await this.metaService.fetch()).emailRequiredForSignup) {
} else if (this.serverSettings.emailRequiredForSignup) {
throw new ApiError(meta.errors.emailRequired);
}

View file

@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@ -146,6 +145,7 @@ export const paramDef = {
properties: {
name: { ...nameSchema, nullable: true },
description: { ...descriptionSchema, nullable: true },
followedMessage: { ...followedMessageSchema, nullable: true },
location: { ...locationSchema, nullable: true },
birthday: { ...birthdaySchema, nullable: true },
listenbrainz: { ...listenbrainzSchema, nullable: true },
@ -284,6 +284,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
@ -400,7 +401,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.backgroundUrl = null;
updates.backgroundBlurhash = null;
}
if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);

View file

@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],

View file

@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks', 'account'],

View file

@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],

View file

@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { webhookEventTypes } from '@/models/Webhook.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
secure: true,
kind: 'read:account',
limit: {
duration: ms('15min'),
max: 60,
},
errors: {
noSuchWebhook: {
message: 'No such webhook.',
code: 'NO_SUCH_WEBHOOK',
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
webhookId: {
type: 'string',
format: 'misskey:id',
},
type: {
type: 'string',
enum: webhookEventTypes,
},
override: {
type: 'object',
properties: {
url: { type: 'string' },
secret: { type: 'string' },
},
},
},
required: ['webhookId', 'type'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private webhookTestService: WebhookTestService,
) {
super(meta, paramDef, async (ps, me) => {
try {
await this.webhookTestService.testUserWebhook({
webhookId: ps.webhookId,
type: ps.type,
override: ps.override,
}, me);
} catch (e) {
if (e instanceof WebhookTestService.NoSuchWebhookError) {
throw new ApiError(meta.errors.noSuchWebhook);
}
throw e;
}
});
}
}

View file

@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';

View file

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@ -75,6 +74,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -88,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
private metaService: MetaService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
) {
super(meta, paramDef, async (ps, me) => {
@ -102,9 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch();
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@ -158,7 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
allowPartial: ps.allowPartial,
me,
redisTimelines: timelineConfig,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
excludeBots: !ps.withBots,

View file

@ -5,16 +5,14 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@ -67,6 +65,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -74,10 +75,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
private cacheService: CacheService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -90,9 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch();
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@ -117,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines:
ps.withFiles ? ['localTimelineWithFiles']
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']

View file

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
export const meta = {
@ -57,6 +56,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -70,15 +72,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private userFollowingService: UserFollowingService,
private queryService: QueryService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const serverSettings = await this.metaService.fetch();
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@ -110,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,

View file

@ -4,14 +4,15 @@
*/
import { URLSearchParams } from 'node:url';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@ -59,9 +60,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private noteEntityService: NoteEntityService,
private getterService: GetterService,
private metaService: MetaService,
private httpRequestService: HttpRequestService,
private roleService: RoleService,
) {
@ -84,13 +87,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return;
}
const instance = await this.metaService.fetch();
if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) {
throw new ApiError(meta.errors.unavailable);
}
if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) {
throw new ApiError(meta.errors.unavailable);
}
@ -98,11 +99,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
const params = new URLSearchParams();
if (instance.deeplAuthKey) params.append('auth_key', instance.deeplAuthKey);
if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
const endpoint = instance.deeplFreeMode && instance.deeplFreeInstance ? instance.deeplFreeInstance : instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
@ -112,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
},
body: params.toString(),
});
if (instance.deeplAuthKey) {
if (this.serverSettings.deeplAuthKey) {
const json = (await res.json()) as {
translations: {
detected_source_language: string;

View file

@ -5,16 +5,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@ -69,6 +67,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -80,11 +81,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -99,9 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList);
}
const serverSettings = await this.metaService.fetch();
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb(list, {
untilId,
sinceId,
@ -124,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,

View file

@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js';
import type { MiMeta, UsersRepository } from '@/models/_.js';
import * as Acct from '@/misc/acct.js';
import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@ -38,16 +37,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private metaService: MetaService,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const meta = await this.metaService.fetch();
const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(),
})));

View file

@ -5,9 +5,10 @@
import * as os from 'node:os';
import si from 'systeminformation';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: false,
@ -73,10 +74,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private metaService: MetaService,
@Inject(DI.meta)
private serverSettings: MiMeta,
) {
super(meta, paramDef, async () => {
if (!(await this.metaService.fetch()).enableServerMachineStats) return {
if (!this.serverSettings.enableServerMachineStats) return {
machine: '?',
cpu: {
model: '?',

View file

@ -5,9 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { IdService } from '@/core/IdService.js';
import type { SwSubscriptionsRepository } from '@/models/_.js';
import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
@ -62,11 +61,13 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
private idService: IdService,
private metaService: MetaService,
private pushNotificationService: PushNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
@ -78,12 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
publickey: ps.publickey,
});
const instance = await this.metaService.fetch(true);
if (exist != null) {
return {
state: 'already-subscribed' as const,
key: instance.swPublicKey,
key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage,
@ -103,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
state: 'subscribed' as const,
key: instance.swPublicKey,
key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: ps.endpoint,
sendReadMessage: ps.sendReadMessage,

View file

@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { localUsernameSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['users'],
@ -39,13 +38,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const exist = await this.usersRepository.countBy({
@ -55,8 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
const meta = await this.metaService.fetch();
const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
return {
available: exist === 0 && exist2 === 0 && !isPreserved,

View file

@ -5,14 +5,13 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
@ -67,6 +66,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -75,15 +77,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isSelf = me && (me.id === ps.userId);
const serverSettings = await this.metaService.fetch();
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
// early return if me is blocked by requesting user
@ -94,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (!serverSettings.enableFanoutTimeline) {
if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,

View file

@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
import { FastifyAdapter } from '@bull-board/fastify';
import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify';
import ms from 'ms';
import sharp from 'sharp';
import pug from 'pug';
@ -24,7 +24,6 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
import type {
DbQueue,
DeliverQueue,
@ -61,7 +60,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
@Injectable()
@ -72,6 +72,9 @@ export class ClientServerService {
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -108,7 +111,6 @@ export class ClientServerService {
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
private metaService: MetaService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
private roleService: RoleService,
@ -128,32 +130,30 @@ export class ClientServerService {
@bindThis
private async manifestHandler(reply: FastifyReply) {
const instance = await this.metaService.fetch(true);
let manifest = {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'short_name': instance.shortName || instance.name || this.config.host,
'short_name': this.meta.shortName || this.meta.name || this.config.host,
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'name': instance.name || this.config.host,
'name': this.meta.name || this.config.host,
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'theme_color': instance.themeColor || '#86b300',
'theme_color': this.meta.themeColor || '#86b300',
'icons': [{
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app192IconUrl || '/static-assets/icons/192.png',
'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app512IconUrl || '/static-assets/icons/512.png',
'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
@ -179,7 +179,7 @@ export class ClientServerService {
manifest = {
...manifest,
...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride),
};
reply.header('Cache-Control', 'max-age=300');
@ -242,7 +242,7 @@ export class ClientServerService {
}
});
const serverAdapter = new FastifyAdapter();
const bullBoardServerAdapter = new BullBoardFastifyAdapter();
createBullBoard({
queues: [
@ -255,11 +255,11 @@ export class ClientServerService {
this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
serverAdapter,
serverAdapter: bullBoardServerAdapter,
});
serverAdapter.setBasePath(bullBoardPath);
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
bullBoardServerAdapter.setBasePath(bullBoardPath);
(fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion
fastify.register(fastifyView, {
@ -280,15 +280,22 @@ export class ClientServerService {
});
//#region vite assets
if (this.config.clientManifestExists) {
if (this.config.frontendEmbedManifestExists) {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
root: viteOut,
root: frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: frontendEmbedViteOut,
prefix: '/embed_vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
});
@ -299,6 +306,13 @@ export class ClientServerService {
prefix: '/vite',
rewritePrefix: '/vite',
});
const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
fastify.register(fastifyProxy, {
upstream: 'http://localhost:' + embedPort,
prefix: '/embed_vite',
rewritePrefix: '/embed_vite',
});
}
//#endregion
@ -443,15 +457,20 @@ export class ClientServerService {
// Manifest
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
// Embed Javascript
fastify.get('/embed.js', async (request, reply) => {
return await reply.sendFile('/embed.js', staticAssets, {
maxAge: ms('1 day'),
});
});
fastify.get('/robots.txt', async (request, reply) => {
return await reply.sendFile('/robots.txt', staticAssets);
});
// OpenSearch XML
fastify.get('/opensearch.xml', async (request, reply) => {
const meta = await this.metaService.fetch();
const name = meta.name ?? 'Sharkey';
const name = this.meta.name ?? 'Sharkey';
let content = '';
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
content += `<ShortName>${name}</ShortName>`;
@ -468,14 +487,13 @@ export class ClientServerService {
//#endregion
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
img: meta.bannerUrl,
img: this.meta.bannerUrl,
url: this.config.url,
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
title: this.meta.name ?? 'Misskey',
desc: this.meta.description,
...await this.generateCommonPugData(this.meta),
...data,
});
};
@ -553,7 +571,6 @@ export class ClientServerService {
if (user != null) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const meta = await this.metaService.fetch();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
@ -569,7 +586,7 @@ export class ClientServerService {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
// リモートユーザーなので
@ -607,7 +624,6 @@ export class ClientServerService {
if (note) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@ -619,7 +635,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -644,7 +660,6 @@ export class ClientServerService {
if (page) {
const _page = await this.pageEntityService.pack(page);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
const meta = await this.metaService.fetch();
if (['public'].includes(page.visibility)) {
reply.header('Cache-Control', 'public, max-age=15');
} else {
@ -658,7 +673,7 @@ export class ClientServerService {
page: _page,
profile,
avatarUrl: _page.user.avatarUrl,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -674,7 +689,6 @@ export class ClientServerService {
if (flash) {
const _flash = await this.flashEntityService.pack(flash);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@ -684,7 +698,7 @@ export class ClientServerService {
flash: _flash,
profile,
avatarUrl: _flash.user.avatarUrl,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -700,7 +714,6 @@ export class ClientServerService {
if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@ -710,7 +723,7 @@ export class ClientServerService {
clip: _clip,
profile,
avatarUrl: _clip.user.avatarUrl,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -724,7 +737,6 @@ export class ClientServerService {
if (post) {
const _post = await this.galleryPostEntityService.pack(post);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@ -734,7 +746,7 @@ export class ClientServerService {
post: _post,
profile,
avatarUrl: _post.user.avatarUrl,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -749,11 +761,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', {
channel: _channel,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -768,11 +779,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', {
game: _game,
...await this.generateCommonPugData(meta),
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@ -780,7 +790,7 @@ export class ClientServerService {
});
//#endregion
//region noindex pages
//#region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
@ -790,21 +800,97 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
//endregion
//#endregion
//#region embed pages
fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const user = await this.usersRepository.findOneBy({
id: request.params.user,
});
if (user == null) return;
if (user.host != null) return;
const _user = await this.userEntityService.pack(user);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
user: _user,
}),
});
});
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const note = await this.notesRepository.findOneBy({
id: request.params.note,
});
if (note == null) return;
if (note.visibility !== 'public') return;
if (note.userHost != null) return;
const _note = await this.noteEntityService.pack(note, null, { detail: true });
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
note: _note,
}),
});
});
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const clip = await this.clipsRepository.findOneBy({
id: request.params.clip,
});
if (clip == null) return;
const _clip = await this.clipEntityService.pack(clip);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
clip: _clip,
}),
});
});
fastify.get('/embed/*', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
...await this.generateCommonPugData(this.meta),
});
});
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
reply.removeHeader('X-Frame-Options');
return await reply.view('info-card', {
version: this.config.version,
host: this.config.host,
meta: meta,
meta: this.meta,
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
});
});
//#endregion
fastify.get('/bios', async (request, reply) => {
return await reply.view('bios', {

View file

@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly';
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.js';
import { query } from '@/misc/prelude/url.js';
@ -32,7 +31,9 @@ export class UrlPreviewService {
@Inject(DI.redis)
private redisClient: Redis.Redis,
private metaService: MetaService,
@Inject(DI.meta)
private meta: MiMeta,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@ -75,9 +76,7 @@ export class UrlPreviewService {
return;
}
const meta = await this.metaService.fetch();
if (!meta.urlPreviewEnabled) {
if (!this.meta.urlPreviewEnabled) {
reply.code(403);
return {
error: new ApiError({
@ -98,14 +97,14 @@ export class UrlPreviewService {
return cached;
}
this.logger.info(meta.urlPreviewSummaryProxyUrl
this.logger.info(this.meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${key} ...`
: `Getting preview of ${key} ...`);
try {
const summary = meta.urlPreviewSummaryProxyUrl
? await this.fetchSummaryFromProxy(url, meta, lang)
: await this.fetchSummary(url, meta, lang);
const summary = this.meta.urlPreviewSummaryProxyUrl
? await this.fetchSummaryFromProxy(url, this.meta, lang)
: await this.fetchSummary(url, this.meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`);

View file

@ -0,0 +1,219 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED');
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE');
};
let forceError = localStorage.getItem('forceError');
if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return;
}
// パラメータに応じてsplashのスタイルを変更
const params = new URLSearchParams(location.search);
if (params.has('rounded') && params.get('rounded') === 'false') {
document.documentElement.classList.add('norounded');
}
if (params.has('border') && params.get('border') === 'false') {
document.documentElement.classList.add('noborder');
}
//#region Detect language & fetch translations
if (!localStorage.hasOwnProperty('locale')) {
const supportedLangs = LANGS;
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
if (lang == null) lang = 'en-US';
}
}
const metaRes = await window.fetch('/api/meta', {
method: 'POST',
body: JSON.stringify({}),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
});
if (metaRes.status !== 200) {
renderError('META_FETCH');
return;
}
const meta = await metaRes.json();
const v = meta.version;
if (v == null) {
renderError('META_FETCH_V');
return;
}
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
if (localRes.status === 200) {
localStorage.setItem('lang', lang);
localStorage.setItem('locale', await localRes.text());
localStorage.setItem('localeVersion', v);
} else {
renderError('LOCALE_FETCH');
return;
}
}
//#endregion
//#region Script
async function importAppScript() {
await import(`/embed_vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
renderError('APP_IMPORT');
});
}
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
if (document.readyState !== 'loading') {
importAppScript();
} else {
window.addEventListener('DOMContentLoaded', () => {
importAppScript();
});
}
//#endregion
async function addStyle(styleText) {
let css = document.createElement('style');
css.appendChild(document.createTextNode(styleText));
document.head.appendChild(css);
}
async function renderError(code) {
// Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<div class="message">読み込みに失敗しました</div>
<div class="submessage">Failed to initialize Misskey</div>
<div class="submessage">Error Code: ${code}</div>
<button onclick="location.reload(!0)">
<div>リロード</div>
<div><small>Reload</small></div>
</button>`;
addStyle(`
#misskey_app,
#splash {
display: none !important;
}
html,
body {
margin: 0;
}
body {
position: relative;
color: #dee7e4;
font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
line-height: 1.35;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 24px;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--radius, 12px);
border: 1px solid rgba(231, 255, 251, 0.14);
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #192320;
border-radius: var(--radius, 12px);
z-index: -1;
}
html.embed.norounded body,
html.embed.norounded body::before {
border-radius: 0;
}
html.embed.noborder body {
border: none;
}
.icon {
max-width: 60px;
width: 100%;
height: auto;
margin-bottom: 20px;
color: #dec340;
}
.message {
text-align: center;
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
.submessage {
text-align: center;
font-size: 90%;
margin-bottom: 7.5px;
}
.submessage:last-of-type {
margin-bottom: 20px;
}
button {
padding: 7px 14px;
min-width: 100px;
font-weight: 700;
font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
line-height: 1.35;
border-radius: 99rem;
background-color: #b4e900;
color: #192320;
border: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
button:hover {
background-color: #c6ff03;
}`);
}
})();

View file

@ -3,17 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* BOOT LOADER
* サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで以下の役割を持ちます
* - 翻訳ファイルをフェッチする
* - バージョンに基づいて適切なメインスクリプトを読み込む
* - キャッシュされたコンパイル済みテーマを適用する
* - クライアントの設定値に基づいて対応するHTMLクラス等を設定する
* テーマをこの段階で設定するのはメインスクリプトが読み込まれる間もテーマを適用したいためです
* : webpackは介さないためこのファイルではrequireやimportは使えません
*/
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@ -192,7 +181,7 @@
if (!errorsElement) {
document.body.innerHTML = `
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
@ -202,10 +191,10 @@
<span class="button-label-big">Reload / リロード</span>
</button>
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります</b></p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
<p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;">
<summary>Other options / その他のオプション</summary>
<a href="/flush">
@ -238,7 +227,7 @@
<summary>
<code>ERROR CODE: ${code}</code>
</summary>
<code>${JSON.stringify(details)}</code>`;
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
errorsElement.appendChild(detailsElement);
addStyle(`
* {
@ -346,6 +335,6 @@
#errorInfo {
width: 50%;
}
}`)
}`);
}
})();

View file

@ -47,6 +47,7 @@ html {
transform: translateY(80px);
color: var(--accent);
}
#splashSpinner > .spinner {
position: absolute;
top: 0;

View file

@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
html {
background-color: var(--bg);
color: var(--fg);
}
html.embed {
box-sizing: border-box;
background-color: transparent;
color-scheme: light dark;
max-width: 500px;
}
#splash {
position: fixed;
z-index: 10000;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: wait;
background-color: var(--bg);
opacity: 1;
transition: opacity 0.5s ease;
}
html.embed #splash {
box-sizing: border-box;
min-height: 300px;
border-radius: var(--radius, 12px);
border: 1px solid var(--divider, #e8e8e8);
}
html.embed.norounded #splash {
border-radius: 0;
}
html.embed.noborder #splash {
border: none;
}
#splashIcon {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 64px;
height: 64px;
pointer-events: none;
}
#splashSpinner {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
display: inline-block;
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
}
#splashSpinner > .spinner {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
#splashSpinner > .spinner.fg {
animation: splashSpinner 0.5s linear infinite;
}
@keyframes splashSpinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,72 @@
block vars
block loadClientEntry
- const entry = config.frontendEmbedEntry;
doctype html
html(class='embed')
head
meta(charset='utf-8')
meta(name='application-name' content='Misskey')
meta(name='referrer' content='origin')
meta(name='theme-color' content= themeColor || '#86b300')
meta(name='theme-color-orig' content= themeColor || '#86b300')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(property='instance_url' content= instanceUrl)
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='modulepreload' href=`/embed_vite/${entry.file}`)
if !config.frontendEmbedManifestExists
script(type="module" src="/embed_vite/@vite/client")
if Array.isArray(entry.css)
each href in entry.css
link(rel='stylesheet' href=`/embed_vite/${href}`)
title
block title
= title || 'Misskey'
block meta
meta(name='robots' content='noindex')
style
include ../style.embed.css
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson
script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
!= embedCtx
script
include ../boot.embed.js
body
noscript: p
| JavaScriptを有効にしてください
br
| Please turn on your JavaScript
div#splash
img#splashIcon(src= icon || '/static-assets/splash.png')
div#splashSpinner
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
block content

View file

@ -1,7 +1,7 @@
block vars
block loadClientEntry
- const clientEntry = config.clientEntry;
- const entry = config.frontendEntry;
doctype html
@ -40,16 +40,13 @@ html
link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href=`/assets/phosphor-icons/bold/style.css?version=${version}`)
link(rel='stylesheet' href=`/static-assets/fonts/sharkey-icons/style.css?version=${version}`)
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
link(rel='modulepreload' href=`/vite/${entry.file}`)
if !config.clientManifestExists
if !config.frontendManifestExists
script(type="module" src="/vite/@vite/client")
if Array.isArray(clientEntry.css)
each href in clientEntry.css
if Array.isArray(entry.css)
each href in entry.css
link(rel='stylesheet' href=`/vite/${href}`)
title
@ -75,7 +72,7 @@ html
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}";
var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson