enhance: metaをSSR HTMLに埋め込む (#13436)

* enhance: `meta`をSSR HTMLに埋め込む

* HTML Metaの有効時間を指定

* 1時間

* MetaEntityService

* JSONをPackするように

* ✌️

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり 2024-02-23 10:47:17 +09:00 committed by GitHub
parent bf5952fd63
commit d20542c495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 676 additions and 530 deletions

View file

@ -116,6 +116,7 @@ import { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
import { RoleEntityService } from './entities/RoleEntityService.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import { MetaEntityService } from './entities/MetaEntityService.js';
import { ApAudienceService } from './activitypub/ApAudienceService.js';
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
@ -254,6 +255,7 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@ -393,6 +395,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService,
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
ApAudienceService,
ApDbResolverService,
@ -528,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService,
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService,
$ApDbResolverService,
@ -663,6 +667,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService,
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
ApAudienceService,
ApDbResolverService,
@ -797,6 +802,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService,
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService,
$ApDbResolverService,

View file

@ -0,0 +1,154 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { Packed } from '@/misc/json-schema.js';
import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@Injectable()
export class MetaEntityService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { }
@bindThis
public async pack(meta?: MiMeta): Promise<Packed<'MetaLite'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const packed: Packed<'MetaLite'> = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
bannerUrl: instance.bannerUrl,
infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
serverRules: instance.serverRules,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
};
return packed;
}
@bindThis
public async packDetailed(meta?: MiMeta): Promise<Packed<'MetaDetailed'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const packed = await this.pack(instance);
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null;
const packDetailed: Packed<'MetaDetailed'> = {
...packed,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
proxyAccountName: proxyAccount ? proxyAccount.username : null,
features: {
localTimeline: instance.policies.ltlAvailable,
globalTimeline: instance.policies.gtlAvailable,
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
},
};
return packDetailed;
}
}

View file

@ -50,6 +50,11 @@ import {
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
import {
packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
} from '@/models/json-schema/meta.js';
export const refs = {
UserLite: packedUserLiteSchema,
@ -99,6 +104,9 @@ export const refs = {
RolePolicies: packedRolePoliciesSchema,
ReversiGameLite: packedReversiGameLiteSchema,
ReversiGameDetailed: packedReversiGameDetailedSchema,
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
};
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;

View file

@ -0,0 +1,328 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedMetaLiteSchema = {
type: 'object',
optional: false, nullable: false,
properties: {
maintainerName: {
type: 'string',
optional: false, nullable: true,
},
maintainerEmail: {
type: 'string',
optional: false, nullable: true,
},
version: {
type: 'string',
optional: false, nullable: false,
},
providesTarball: {
type: 'boolean',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: true,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
uri: {
type: 'string',
optional: false, nullable: false,
format: 'url',
example: 'https://misskey.example.com',
},
description: {
type: 'string',
optional: false, nullable: true,
},
langs: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
tosUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey',
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey/issues/new',
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
},
defaultLightTheme: {
type: 'string',
optional: false, nullable: true,
},
disableRegistration: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableTurnstile: {
type: 'boolean',
optional: false, nullable: false,
},
turnstileSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
},
mascotImageUrl: {
type: 'string',
optional: false, nullable: false,
default: '/assets/ai.png',
},
bannerUrl: {
type: 'string',
optional: false, nullable: true,
},
serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
place: {
type: 'string',
optional: false, nullable: false,
},
ratio: {
type: 'number',
optional: false, nullable: false,
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
dayOfWeek: {
type: 'integer',
optional: false, nullable: false,
},
},
},
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
translatorAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
backgroundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
impressumUrl: {
type: 'string',
optional: false, nullable: true,
},
logoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
privacyPolicyUrl: {
type: 'string',
optional: false, nullable: true,
},
serverRules: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
},
} as const;
export const packedMetaDetailedOnlySchema = {
type: 'object',
optional: false, nullable: false,
properties: {
features: {
type: 'object',
optional: true, nullable: false,
properties: {
registration: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
globalTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
turnstile: {
type: 'boolean',
optional: false, nullable: false,
},
recaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
objectStorage: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
miauth: {
type: 'boolean',
optional: true, nullable: false,
default: true,
},
},
},
proxyAccountName: {
type: 'string',
optional: false, nullable: true,
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
cacheRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;
export const packedMetaDetailedSchema = {
type: 'object',
allOf: [
{
type: 'object',
ref: 'MetaLite',
},
{
type: 'object',
ref: 'MetaDetailedOnly',
},
],
} as const;

View file

@ -3,18 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
export const meta = {
tags: ['meta'],
@ -23,297 +14,10 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
properties: {
maintainerName: {
type: 'string',
optional: false, nullable: true,
},
maintainerEmail: {
type: 'string',
optional: false, nullable: true,
},
version: {
type: 'string',
optional: false, nullable: false,
},
providesTarball: {
type: 'boolean',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
uri: {
type: 'string',
optional: false, nullable: false,
format: 'url',
example: 'https://misskey.example.com',
},
description: {
type: 'string',
optional: false, nullable: true,
},
langs: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
tosUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey',
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey/issues/new',
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
},
defaultLightTheme: {
type: 'string',
optional: false, nullable: true,
},
disableRegistration: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableTurnstile: {
type: 'boolean',
optional: false, nullable: false,
},
turnstileSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
},
mascotImageUrl: {
type: 'string',
optional: false, nullable: false,
default: '/assets/ai.png',
},
bannerUrl: {
type: 'string',
optional: false, nullable: false,
},
serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
place: {
type: 'string',
optional: false, nullable: false,
},
ratio: {
type: 'number',
optional: false, nullable: false,
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
dayOfWeek: {
type: 'integer',
optional: false, nullable: false,
},
},
},
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
translatorAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
proxyAccountName: {
type: 'string',
optional: false, nullable: true,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
features: {
type: 'object',
optional: true, nullable: false,
properties: {
registration: {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
globalTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
objectStorage: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
miauth: {
type: 'boolean',
optional: true, nullable: false,
default: true,
},
},
},
backgroundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
impressumUrl: {
type: 'string',
optional: false, nullable: true,
},
logoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
privacyPolicyUrl: {
type: 'string',
optional: false, nullable: true,
},
serverRules: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
},
oneOf: [
{ type: 'object', ref: 'MetaLite' },
{ type: 'object', ref: 'MetaDetailed' },
],
},
} as const;
@ -328,115 +32,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
private metaEntityService: MetaEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const response: any = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
serverRules: instance.serverRules,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
} : {}),
};
if (ps.detail) {
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null;
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
response.features = {
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
}
return response;
return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack();
});
}
}

View file

@ -28,6 +28,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
@ -93,6 +94,7 @@ export class ClientServerService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private pageEntityService: PageEntityService,
private metaEntityService: MetaEntityService,
private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
@ -173,7 +175,7 @@ export class ClientServerService {
}
@bindThis
private generateCommonPugData(meta: MiMeta) {
private async generateCommonPugData(meta: MiMeta) {
return {
instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl,
@ -183,6 +185,8 @@ export class ClientServerService {
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
instanceUrl: this.config.url,
metaJson: JSON.stringify(await this.metaEntityService.packDetailed(meta)),
now: Date.now(),
};
}
@ -433,7 +437,7 @@ export class ClientServerService {
url: this.config.url,
title: meta.name ?? 'Misskey',
desc: meta.description,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
};
@ -520,7 +524,7 @@ export class ClientServerService {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
// リモートユーザーなので
@ -570,7 +574,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -609,7 +613,7 @@ export class ClientServerService {
page: _page,
profile,
avatarUrl: _page.user.avatarUrl,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -635,7 +639,7 @@ export class ClientServerService {
flash: _flash,
profile,
avatarUrl: _flash.user.avatarUrl,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -661,7 +665,7 @@ export class ClientServerService {
clip: _clip,
profile,
avatarUrl: _clip.user.avatarUrl,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -685,7 +689,7 @@ export class ClientServerService {
post: _post,
profile,
avatarUrl: _post.user.avatarUrl,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -704,7 +708,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', {
channel: _channel,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@ -723,7 +727,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', {
game: _game,
...this.generateCommonPugData(meta),
...await this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);

View file

@ -68,6 +68,9 @@ html
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson
script
include ../boot.js

View file

@ -11,7 +11,7 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js';
import { fetchInstance, instance } from '@/instance.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -235,12 +235,10 @@ export async function mainBoot() {
}
}
fetchInstance().then(() => {
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
}
});
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
}
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト

View file

@ -11,13 +11,24 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
// TODO: 他のタブと永続化されたstateを同期
const cached = miLocalStorage.getItem('instance');
//#region loader
const providedMetaEl = document.getElementById('misskey_meta');
let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
const providedMeta = providedMetaEl && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
const providedAt = providedMetaEl && providedMetaEl.dataset.generatedAt ? parseInt(providedMetaEl.dataset.generatedAt) : 0;
if (providedAt > cachedAt) {
miLocalStorage.setItem('instance', JSON.stringify(providedMeta));
miLocalStorage.setItem('instanceCachedAt', providedAt.toString());
cachedMeta = providedMeta;
cachedAt = providedAt;
}
//#endregion
// TODO: instanceをリアクティブにするかは再考の余地あり
export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : {
// TODO: set default values
});
export const instance: Misskey.entities.MetaResponse = reactive(cachedMeta ?? {});
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
@ -25,7 +36,15 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
export async function fetchInstance() {
export async function fetchInstance(force = false): Promise<void> {
if (!force) {
const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
if (Date.now() - cachedAt < 1000 * 60 * 60) {
return;
}
}
const meta = await misskeyApi('meta', {
detail: false,
});
@ -35,4 +54,5 @@ export async function fetchInstance() {
}
miLocalStorage.setItem('instance', JSON.stringify(instance));
miLocalStorage.setItem('instanceCachedAt', Date.now().toString());
}

View file

@ -7,6 +7,7 @@ type Keys =
'v' |
'lastVersion' |
'instance' |
'instanceCachedAt' |
'account' |
'accounts' |
'latestDonationInfoShownAt' |

View file

@ -142,7 +142,7 @@ function save() {
turnstileSiteKey: turnstileSiteKey.value,
turnstileSecretKey: turnstileSecretKey.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}
</script>

View file

@ -169,7 +169,7 @@ function save() {
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -124,7 +124,7 @@ function save() {
smtpUser: smtpUser.value,
smtpPass: smtpPass.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -61,7 +61,7 @@ function save() {
deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -50,7 +50,7 @@ function save() {
silencedHosts: silencedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -110,7 +110,7 @@ function save() {
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -143,7 +143,7 @@ function save() {
objectStorageSetPublicRead: objectStorageSetPublicRead.value,
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -73,7 +73,7 @@ function save() {
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -56,7 +56,7 @@ function save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -196,7 +196,7 @@ async function init() {
enableTruemailApi.value = meta.enableTruemailApi;
truemailInstance.value = meta.truemailInstance;
truemailAuthKey.value = meta.truemailAuthKey;
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
}
function save() {
@ -221,7 +221,7 @@ function save() {
truemailAuthKey: truemailAuthKey.value,
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}

View file

@ -58,7 +58,7 @@ const save = async () => {
await os.apiWithDialog('admin/update-meta', {
serverRules: serverRules.value,
});
fetchInstance();
fetchInstance(true);
};
const remove = (index: number): void => {

View file

@ -243,7 +243,7 @@ async function save(): void {
notesPerOneAd: notesPerOneAd.value,
});
fetchInstance();
fetchInstance(true);
}
const headerTabs = computed(() => []);

View file

@ -2,14 +2,18 @@ import { unisonReload } from '@/scripts/unison-reload.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { fetchInstance } from '@/instance.js';
export async function clearCache() {
os.waiting();
miLocalStorage.removeItem('instance');
miLocalStorage.removeItem('instanceCachedAt');
miLocalStorage.removeItem('locale');
miLocalStorage.removeItem('localeVersion');
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
await fetchInstance(true);
await fetchCustomEmojis(true);
unisonReload();
}

View file

@ -1715,7 +1715,10 @@ declare namespace entities {
Role,
RolePolicies,
ReversiGameLite,
ReversiGameDetailed
ReversiGameDetailed,
MetaLite,
MetaDetailedOnly,
MetaDetailed
}
}
export { entities }
@ -2223,6 +2226,15 @@ type MeDetailed = components['schemas']['MeDetailed'];
// @public (undocumented)
type MeDetailedOnly = components['schemas']['MeDetailedOnly'];
// @public (undocumented)
type MetaDetailed = components['schemas']['MetaDetailed'];
// @public (undocumented)
type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
// @public (undocumented)
type MetaLite = components['schemas']['MetaLite'];
// @public (undocumented)
type MetaRequest = operations['meta']['requestBody']['content']['application/json'];

View file

@ -46,3 +46,6 @@ export type Role = components['schemas']['Role'];
export type RolePolicies = components['schemas']['RolePolicies'];
export type ReversiGameLite = components['schemas']['ReversiGameLite'];
export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
export type MetaLite = components['schemas']['MetaLite'];
export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
export type MetaDetailed = components['schemas']['MetaDetailed'];

View file

@ -4724,6 +4724,96 @@ export type components = {
logs: number[][];
map: string[];
};
MetaLite: {
maintainerName: string | null;
maintainerEmail: string | null;
version: string;
providesTarball: boolean;
name: string | null;
shortName: string | null;
/**
* Format: url
* @example https://misskey.example.com
*/
uri: string;
description: string | null;
langs: string[];
tosUrl: string | null;
/** @default https://github.com/misskey-dev/misskey */
repositoryUrl: string | null;
/** @default https://github.com/misskey-dev/misskey/issues/new */
feedbackUrl: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
disableRegistration: boolean;
emailRequiredForSignup: boolean;
enableHcaptcha: boolean;
hcaptchaSiteKey: string | null;
enableMcaptcha: boolean;
mcaptchaSiteKey: string | null;
mcaptchaInstanceUrl: string | null;
enableRecaptcha: boolean;
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
bannerUrl: string | null;
serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null;
maxNoteTextLength: number;
ads: {
/**
* Format: id
* @example xxxxxxxxxx
*/
id: string;
/** Format: url */
url: string;
place: string;
ratio: number;
/** Format: url */
imageUrl: string;
dayOfWeek: number;
}[];
/** @default 0 */
notesPerOneAd: number;
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
mediaProxy: string;
backgroundImageUrl: string | null;
impressumUrl: string | null;
logoImageUrl: string | null;
privacyPolicyUrl: string | null;
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
};
MetaDetailedOnly: {
features?: {
registration: boolean;
emailRequiredForSignup: boolean;
localTimeline: boolean;
globalTimeline: boolean;
hcaptcha: boolean;
turnstile: boolean;
recaptcha: boolean;
objectStorage: boolean;
serviceWorker: boolean;
/** @default true */
miauth?: boolean;
};
proxyAccountName: string | null;
/** @example false */
requireSetup: boolean;
cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
};
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
};
responses: never;
parameters: never;
@ -19448,91 +19538,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': {
maintainerName: string | null;
maintainerEmail: string | null;
version: string;
providesTarball: boolean;
name: string;
shortName: string | null;
/**
* Format: url
* @example https://misskey.example.com
*/
uri: string;
description: string | null;
langs: string[];
tosUrl: string | null;
/** @default https://github.com/misskey-dev/misskey */
repositoryUrl: string | null;
/** @default https://github.com/misskey-dev/misskey/issues/new */
feedbackUrl: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
disableRegistration: boolean;
cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
emailRequiredForSignup: boolean;
enableHcaptcha: boolean;
hcaptchaSiteKey: string | null;
enableMcaptcha: boolean;
mcaptchaSiteKey: string | null;
mcaptchaInstanceUrl: string | null;
enableRecaptcha: boolean;
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
bannerUrl: string;
serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null;
maxNoteTextLength: number;
ads: {
/**
* Format: id
* @example xxxxxxxxxx
*/
id: string;
/** Format: url */
url: string;
place: string;
ratio: number;
/** Format: url */
imageUrl: string;
dayOfWeek: number;
}[];
/** @default 0 */
notesPerOneAd: number;
/** @example false */
requireSetup: boolean;
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
proxyAccountName: string | null;
mediaProxy: string;
features?: {
registration: boolean;
localTimeline: boolean;
globalTimeline: boolean;
hcaptcha: boolean;
recaptcha: boolean;
objectStorage: boolean;
serviceWorker: boolean;
/** @default true */
miauth?: boolean;
};
backgroundImageUrl: string | null;
impressumUrl: string | null;
logoImageUrl: string | null;
privacyPolicyUrl: string | null;
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
};
'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed'];
};
};
/** @description Client error */