Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-07-14 01:01:51 +08:00 committed by GitHub
commit 07d58fd0ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 582 additions and 475 deletions

View File

@ -27,9 +27,11 @@
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
- プロフィールURLをコピーできるボタンを追加 #11190
- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript)
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
- オフライン時の画面にリロードボタンを追加
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正

2
locales/index.d.ts vendored
View File

@ -2158,4 +2158,4 @@ export interface Locale {
declare const locales: {
[lang: string]: Locale;
};
export = locales;
export default locales;

View File

@ -103,7 +103,7 @@ export class FetchInstanceMetadataService {
if (name) updates.name = name;
if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon;
if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor;

View File

@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.reply) {
// 通知
if (data.reply.userHost === null) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
}
});
if (!threadMuted) {
if (!isThreadMuted) {
nm.push(data.reply.userId, 'reply');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
userId: u.id,
threadId: note.threadId ?? note.id,
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,
},
});
if (threadMuted) {
if (isThreadMuted) {
continue;
}

View File

@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
//#endregion
// スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({
userId: userId,
threadId: note.threadId ?? note.id,
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
},
});
if (threadMute) return;
if (isThreadMuted) return;
const unread = {
id: this.idService.genId(),
@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
if (exist == null) return;
if (!exist) return;
if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);

View File

@ -71,12 +71,12 @@ export class SignupService {
const secret = generateUserToken();
// Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME');
}

View File

@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
const isFollowing = await this.followingsRepository.exist({
where: {
followerId: follower.id,
followeeId: followee.id,
},
});
if (following) {
if (isFollowing) {
autoAccept = true;
}
// フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const followed = await this.followingsRepository.findOneBy({
followerId: followee.id,
followeeId: follower.id,
const isFollowed = await this.followingsRepository.exist({
where: {
followerId: followee.id,
followeeId: follower.id,
},
});
if (followed) autoAccept = true;
if (isFollowed) autoAccept = true;
}
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
const requestExist = await this.followRequestsRepository.exist({
where: {
followeeId: followee.id,
followerId: follower.id,
},
});
if (req) {
if (requestExist) {
await this.followRequestsRepository.delete({
followeeId: followee.id,
followerId: follower.id,
@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
}
}
const request = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
const requestExist = await this.followRequestsRepository.exist({
where: {
followeeId: followee.id,
followerId: follower.id,
},
});
if (request == null) {
if (!requestExist) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
}

View File

@ -16,6 +16,8 @@ type AudienceInfo = {
visibleUsers: User[],
};
type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>;
@Injectable()
export class ApAudienceService {
constructor(
@ -67,11 +69,11 @@ export class ApAudienceService {
}
@bindThis
private groupingAudience(ids: string[], actor: RemoteUser) {
const groups = {
public: [] as string[],
followers: [] as string[],
other: [] as string[],
private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience {
const groups: GroupedAudience = {
public: [],
followers: [],
other: [],
};
for (const id of ids) {
@ -90,7 +92,7 @@ export class ApAudienceService {
}
@bindThis
private isPublic(id: string) {
private isPublic(id: string): boolean {
return [
'https://www.w3.org/ns/activitystreams#Public',
'as#Public',
@ -99,9 +101,7 @@ export class ApAudienceService {
}
@bindThis
private isFollowers(id: string, actor: RemoteUser) {
return (
id === (actor.followersUri ?? `${actor.uri}/followers`)
);
private isFollowers(id: string, actor: RemoteUser): boolean {
return id === (actor.followersUri ?? `${actor.uri}/followers`);
}
}

View File

@ -99,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown {
if (parsed.local) {
if (parsed.type !== 'users') return null;
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
return await this.cacheService.userByIdCache.fetchMaybe(
parsed.id,
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
) as LocalUser | undefined ?? null;
} else {
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri,
})) as RemoteUser | null;
return await this.cacheService.uriPersonCache.fetch(
parsed.uri,
() => this.usersRepository.findOneBy({ uri: parsed.uri }),
) as RemoteUser | null;
}
}
@ -145,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
} | null> {
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
if (user == null) return null;
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
const key = await this.publicKeyByUserIdCache.fetch(
user.id,
() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
v => v != null,
);
return {
user,

View File

@ -29,6 +29,121 @@ const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
recipe.type === 'Direct';
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
private userEntityService: UserEntityService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: IActivity | null,
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
@bindThis
public addFollowersRecipe(): void {
const deliver: IFollowersRecipe = {
type: 'Followers',
};
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
@bindThis
public addDirectRecipe(to: RemoteUser): void {
const recipe: IDirectRecipe = {
type: 'Direct',
to,
};
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
@bindThis
public addRecipe(recipe: IRecipe): void {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
@bindThis
public async execute(): Promise<void> {
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
// build inbox list
// Process follower recipes first to avoid duplication when processing direct recipes later.
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
});
for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox;
if (inbox === null) throw new Error('inbox is null');
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
for (const recipe of this.recipes.filter(isDirect)) {
// check that shared inbox has not been added yet
if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue;
// check that they actually have an inbox
if (recipe.to.inbox === null) continue;
inboxes.set(recipe.to.inbox, false);
}
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}
@Injectable()
export class ApDeliverManagerService {
constructor(
@ -52,7 +167,7 @@ export class ApDeliverManagerService {
* @param activity Activity
*/
@bindThis
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
@ -71,7 +186,7 @@ export class ApDeliverManagerService {
* @param to Target user
*/
@bindThis
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
@ -84,7 +199,7 @@ export class ApDeliverManagerService {
}
@bindThis
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager(
this.userEntityService,
this.followingsRepository,
@ -95,123 +210,3 @@ export class ApDeliverManagerService {
);
}
}
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
private userEntityService: UserEntityService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: IActivity | null,
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
@bindThis
public addFollowersRecipe() {
const deliver = {
type: 'Followers',
} as IFollowersRecipe;
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
@bindThis
public addDirectRecipe(to: RemoteUser) {
const recipe = {
type: 'Direct',
to,
} as IDirectRecipe;
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
@bindThis
public addRecipe(recipe: IRecipe) {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
@bindThis
public async execute() {
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
/*
build inbox list
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
this.recipes.filter((recipe): recipe is IDirectRecipe =>
// followers recipes have already been processed
isDirect(recipe)
// check that shared inbox has not been added yet
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
// check that they actually have an inbox
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}

View File

@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@ -86,7 +86,7 @@ export class ApInboxService {
}
@bindThis
public async performActivity(actor: RemoteUser, activity: IObject) {
public async performActivity(actor: RemoteUser, activity: IObject): Promise<void> {
if (isCollectionOrOrderedCollection(activity)) {
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
@ -107,7 +107,7 @@ export class ApInboxService {
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
this.apPersonService.updatePerson(actor.uri!);
this.apPersonService.updatePerson(actor.uri);
});
}
}
@ -229,7 +229,7 @@ export class ApInboxService {
@bindThis
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
@ -273,7 +273,7 @@ export class ApInboxService {
const unlock = await this.appLockService.getApLock(uri);
try {
// 既に同じURIを持つものが登録されていないかチェック
// 既に同じURIを持つものが登録されていないかチェック
const exist = await this.apNoteService.fetchNote(uri);
if (exist) {
return;
@ -292,7 +292,7 @@ export class ApInboxService {
return;
}
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`);
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
}
throw err;
}
@ -409,7 +409,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
@ -420,7 +420,7 @@ export class ApInboxService {
// typeが不明だけど、どうせ消えてるのでremote resolveしない
formerType = undefined;
} else {
const object = activity.object as IObject;
const object = activity.object;
if (isTombstone(object)) {
formerType = toSingle(object.formerType);
} else {
@ -503,7 +503,10 @@ export class ApInboxService {
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!);
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter((userId): userId is string => userId !== undefined);
const users = await this.usersRepository.findBy({
id: In(userIds),
});
@ -566,7 +569,7 @@ export class ApInboxService {
@bindThis
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
if ('actor' in activity && actor.uri !== activity.actor) {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
@ -586,7 +589,7 @@ export class ApInboxService {
@bindThis
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) {
if (actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
@ -618,12 +621,14 @@ export class ApInboxService {
return 'skip: follower not found';
}
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: actor.id,
const isFollowing = await this.followingsRepository.exist({
where: {
followerId: follower.id,
followeeId: actor.id,
},
});
if (following) {
if (isFollowing) {
await this.userFollowingService.unfollow(follower, actor);
return 'ok: unfollowed';
}
@ -673,22 +678,26 @@ export class ApInboxService {
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
}
const req = await this.followRequestsRepository.findOneBy({
followerId: actor.id,
followeeId: followee.id,
const requestExist = await this.followRequestsRepository.exist({
where: {
followerId: actor.id,
followeeId: followee.id,
},
});
const following = await this.followingsRepository.findOneBy({
followerId: actor.id,
followeeId: followee.id,
const isFollowing = await this.followingsRepository.exist({
where: {
followerId: actor.id,
followeeId: followee.id,
},
});
if (req) {
if (requestExist) {
await this.userFollowingService.cancelFollowRequest(followee, actor);
return 'ok: follow request canceled';
}
if (following) {
if (isFollowing) {
await this.userFollowingService.unfollow(actor, followee);
return 'ok: unfollowed';
}
@ -713,7 +722,7 @@ export class ApInboxService {
@bindThis
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
if ('actor' in activity && actor.uri !== activity.actor) {
if (actor.uri !== activity.actor) {
return 'skip: invalid actor';
}
@ -727,7 +736,7 @@ export class ApInboxService {
});
if (isActor(object)) {
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
await this.apPersonService.updatePerson(actor.uri, resolver, object);
return 'ok: Person updated';
} else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));

View File

@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MfmService } from '@/core/MfmService.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js';
import type { IObject } from './type.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApMfmService {
@ -19,14 +19,13 @@ export class ApMfmService {
}
@bindThis
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
public htmlToMfm(html: string, tag?: IObject | IObject[]): string {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name);
return this.mfmService.fromHtml(html, hashtagNames);
}
@bindThis
public getNoteHtml(note: Note) {
public getNoteHtml(note: Note): string | null {
if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
}

View File

@ -1,6 +1,6 @@
import { createPublicKey } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { In, IsNull } from 'typeorm';
import { In } from 'typeorm';
import { v4 as uuid } from 'uuid';
import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js';
@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IIdentifier } from './models/identifier.js';
@Injectable()
export class ApRendererService {
@ -63,7 +62,7 @@ export class ApRendererService {
}
@bindThis
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
return {
type: 'Accept',
actor: this.userEntityService.genLocalUserUri(user.id),
@ -72,7 +71,7 @@ export class ApRendererService {
}
@bindThis
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
return {
type: 'Add',
actor: this.userEntityService.genLocalUserUri(user.id),
@ -82,7 +81,7 @@ export class ApRendererService {
}
@bindThis
public renderAnnounce(object: any, note: Note): IAnnounce {
public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
let to: string[] = [];
@ -133,13 +132,13 @@ export class ApRendererService {
@bindThis
public renderCreate(object: IObject, note: Note): ICreate {
const activity = {
const activity: ICreate = {
id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create',
published: note.createdAt.toISOString(),
object,
} as ICreate;
};
if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc;
@ -209,7 +208,7 @@ export class ApRendererService {
* @param id Follower|Followee ID
*/
@bindThis
public async renderFollowUser(id: User['id']) {
public async renderFollowUser(id: User['id']): Promise<string> {
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
return this.userEntityService.getUserUri(user);
}
@ -223,8 +222,8 @@ export class ApRendererService {
return {
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow',
actor: this.userEntityService.getUserUri(follower)!,
object: this.userEntityService.getUserUri(followee)!,
actor: this.userEntityService.getUserUri(follower),
object: this.userEntityService.getUserUri(followee),
};
}
@ -264,14 +263,14 @@ export class ApRendererService {
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
const reaction = noteReaction.reaction;
const object = {
const object: ILike = {
type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`,
actor: `${this.config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
content: reaction,
_misskey_reaction: reaction,
} as ILike;
};
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
@ -287,7 +286,7 @@ export class ApRendererService {
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
return {
type: 'Mention',
href: this.userEntityService.getUserUri(mention)!,
href: this.userEntityService.getUserUri(mention),
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
};
}
@ -297,8 +296,8 @@ export class ApRendererService {
src: PartialLocalUser | PartialRemoteUser,
dst: PartialLocalUser | PartialRemoteUser,
): IMove {
const actor = this.userEntityService.getUserUri(src)!;
const target = this.userEntityService.getUserUri(dst)!;
const actor = this.userEntityService.getUserUri(src);
const target = this.userEntityService.getUserUri(dst);
return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor,
@ -310,10 +309,10 @@ export class ApRendererService {
@bindThis
public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => {
if (!ids || ids.length === 0) return [];
const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
};
let inReplyTo;
@ -323,9 +322,9 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
if (inReplyToUser != null) {
if (inReplyToUserExist) {
if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri;
} else {
@ -375,7 +374,7 @@ export class ApRendererService {
id: In(note.mentions),
}) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
const files = await getPromisedFiles(note.fileIds);
@ -451,37 +450,26 @@ export class ApRendererService {
@bindThis
public async renderPerson(user: LocalUser) {
const id = this.userEntityService.genLocalUserUri(user.id);
const isSystem = !!user.username.match(/\./);
const isSystem = user.username.includes('.');
const [avatar, banner, profile] = await Promise.all([
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined),
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined),
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]);
const attachment: {
const attachment = profile.fields.map(field => ({
type: 'PropertyValue',
name: string,
value: string,
identifier?: IIdentifier,
}[] = [];
if (profile.fields) {
for (const field of profile.fields) {
attachment.push({
type: 'PropertyValue',
name: field.name,
value: (field.value != null && field.value.match(/^https?:/))
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
: field.value,
});
}
}
name: field.name,
value: /^https?:/.test(field.value)
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
: field.value,
}));
const emojis = await this.getEmojis(user.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
const tag = [
...apemojis,
@ -490,7 +478,7 @@ export class ApRendererService {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const person = {
const person: any = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
id,
inbox: `${id}/inbox`,
@ -508,11 +496,11 @@ export class ApRendererService {
image: banner ? this.renderImage(banner) : null,
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: !!user.isExplorable,
discoverable: user.isExplorable,
publicKey: this.renderKey(user, keypair, '#main-key'),
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined,
} as any;
};
if (user.movedToUri) {
person.movedTo = user.movedToUri;
@ -552,7 +540,7 @@ export class ApRendererService {
}
@bindThis
public renderReject(object: any, user: { id: User['id'] }): IReject {
public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
return {
type: 'Reject',
actor: this.userEntityService.genLocalUserUri(user.id),
@ -561,7 +549,7 @@ export class ApRendererService {
}
@bindThis
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
return {
type: 'Remove',
actor: this.userEntityService.genLocalUserUri(user.id),
@ -579,8 +567,8 @@ export class ApRendererService {
}
@bindThis
public renderUndo(object: any, user: { id: User['id'] }): IUndo {
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
return {
type: 'Undo',
@ -592,7 +580,7 @@ export class ApRendererService {
}
@bindThis
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
return {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: this.userEntityService.genLocalUserUri(user.id),
@ -658,7 +646,7 @@ export class ApRendererService {
vcard: 'http://www.w3.org/2006/vcard/ns#',
},
],
}, x as T & { id: string; });
}, x as T & { id: string });
}
@bindThis
@ -683,13 +671,13 @@ export class ApRendererService {
*/
@bindThis
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
const page = {
const page: any = {
id,
partOf,
type: 'OrderedCollectionPage',
totalItems,
orderedItems,
} as any;
};
if (prev) page.prev = prev;
if (next) page.next = next;
@ -706,7 +694,7 @@ export class ApRendererService {
* @param orderedItems attached objects (optional)
*/
@bindThis
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) {
public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
const page: any = {
id,
type: 'OrderedCollection',
@ -722,7 +710,7 @@ export class ApRendererService {
@bindThis
private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);

View File

@ -140,7 +140,7 @@ export class ApRequestService {
}
@bindThis
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
const body = JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -169,7 +169,7 @@ export class ApRequestService {
* @param url URL to fetch
*/
@bindThis
public async signedGet(url: string, user: { id: User['id'] }) {
public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({

View File

@ -61,10 +61,6 @@ export class Resolver {
@bindThis
public async resolve(value: string | IObject): Promise<IObject> {
if (value == null) {
throw new Error('resolvee is null (or undefined)');
}
if (typeof value !== 'string') {
return value;
}
@ -104,11 +100,11 @@ export class Resolver {
? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
if (object == null || (
if (
Array.isArray(object['@context']) ?
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
)) {
) {
throw new Error('invalid response');
}

View File

@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js';
import type { JsonLdDocument } from 'jsonld';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
@ -18,22 +20,21 @@ class LdSignature {
@bindThis
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
const options = {
type: 'RsaSignature2017',
creator,
domain,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
} as {
const options: {
type: string;
creator: string;
domain?: string;
nonce: string;
created: string;
} = {
type: 'RsaSignature2017',
creator,
nonce: crypto.randomBytes(16).toString('hex'),
created: (created ?? new Date()).toISOString(),
};
if (!domain) {
delete options.domain;
if (domain) {
options.domain = domain;
}
const toBeSigned = await this.createVerifyData(data, options);
@ -62,7 +63,7 @@ class LdSignature {
}
@bindThis
public async createVerifyData(data: any, options: any) {
public async createVerifyData(data: any, options: any): Promise<string> {
const transformedOptions = {
...options,
'@context': 'https://w3id.org/identity/v1',
@ -82,7 +83,7 @@ class LdSignature {
}
@bindThis
public async normalize(data: any) {
public async normalize(data: JsonLdDocument): Promise<string> {
const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
@ -93,14 +94,14 @@ class LdSignature {
@bindThis
private getLoader() {
return async (url: string): Promise<any> => {
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
return async (url: string): Promise<RemoteDocument> => {
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
if (url in CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`);
return {
contextUrl: null,
contextUrl: undefined,
document: CONTEXTS[url],
documentUrl: url,
};
@ -110,7 +111,7 @@ class LdSignature {
if (this.debug) console.debug(`MISS: ${url}`);
const document = await this.fetchDocument(url);
return {
contextUrl: null,
contextUrl: undefined,
document: document,
documentUrl: url,
};
@ -118,13 +119,17 @@ class LdSignature {
}
@bindThis
private async fetchDocument(url: string) {
const json = await this.httpRequestService.send(url, {
headers: {
Accept: 'application/ld+json, application/json',
private async fetchDocument(url: string): Promise<JsonLd> {
const json = await this.httpRequestService.send(
url,
{
headers: {
Accept: 'application/ld+json, application/json',
},
timeout: this.loderTimeout,
},
timeout: this.loderTimeout,
}, { throwErrorWhenResponseNotOk: false }).then(res => {
{ throwErrorWhenResponseNotOk: false },
).then(res => {
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
} else {
@ -132,7 +137,7 @@ class LdSignature {
}
});
return json;
return json as JsonLd;
}
@bindThis

View File

@ -1,3 +1,5 @@
import type { JsonLd } from 'jsonld/jsonld-spec.js';
/* eslint:disable:quotemark indent */
const id_v1 = {
'@context': {
@ -86,7 +88,7 @@ const id_v1 = {
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
},
};
} satisfies JsonLd;
const security_v1 = {
'@context': {
@ -137,7 +139,7 @@ const security_v1 = {
'signatureAlgorithm': 'sec:signingAlgorithm',
'signatureValue': 'sec:signatureValue',
},
};
} satisfies JsonLd;
const activitystreams = {
'@context': {
@ -517,9 +519,9 @@ const activitystreams = {
'@type': '@id',
},
},
};
} satisfies JsonLd;
export const CONTEXTS: Record<string, unknown> = {
export const CONTEXTS: Record<string, JsonLd> = {
'https://w3id.org/identity/v1': id_v1,
'https://w3id.org/security/v1': security_v1,
'https://www.w3.org/ns/activitystreams': activitystreams,

View File

@ -200,7 +200,7 @@ export class ApNoteService {
| { status: 'ok'; res: Note }
| { status: 'permerror' | 'temperror' }
> => {
if (!uri.match(/^https?:/)) return { status: 'permerror' };
if (!/^https?:/.test(uri)) return { status: 'permerror' };
try {
const res = await this.resolveNote(uri);
if (res == null) return { status: 'permerror' };

View File

@ -194,7 +194,6 @@ export interface IApPropertyValue extends IObject {
}
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object &&
getApType(object) === 'PropertyValue' &&
typeof object.name === 'string' &&
'value' in object &&

View File

@ -47,17 +47,26 @@ export class ChannelEntityService {
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
where: {
noteChannelId: channel.id,
userId: meId
},
}) : undefined;
const following = meId ? await this.channelFollowingsRepository.findOneBy({
followerId: meId,
followeeId: channel.id,
}) : null;
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
where: {
followerId: meId,
followeeId: channel.id,
},
}) : false;
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
userId: meId,
channelId: channel.id,
}) : null;
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
where: {
userId: meId,
channelId: channel.id,
},
}) : false;
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
where: {
@ -80,8 +89,8 @@ export class ChannelEntityService {
notesCount: channel.notesCount,
...(me ? {
isFollowing: following != null,
isFavorited: favorite != null,
isFollowing,
isFavorited,
hasUnreadNote,
} : {}),

View File

@ -39,7 +39,7 @@ export class ClipEntityService {
description: clip.description,
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
});
}

View File

@ -40,7 +40,7 @@ export class FlashEntityService {
summary: flash.summary,
script: flash.script,
likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
});
}

View File

@ -46,7 +46,7 @@ export class GalleryPostEntityService {
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
});
}

View File

@ -106,16 +106,14 @@ export class NoteEntityService implements OnModuleInit {
hide = false;
} else {
// フォロワーかどうか
const following = await this.followingsRepository.findOneBy({
followeeId: packedNote.userId,
followerId: meId,
const isFollowing = await this.followingsRepository.exist({
where: {
followeeId: packedNote.userId,
followerId: meId,
},
});
if (following == null) {
hide = true;
} else {
hide = false;
}
hide = !isFollowing;
}
}

View File

@ -97,7 +97,7 @@ export class PageEntityService {
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
});
}

View File

@ -230,12 +230,14 @@ export class UserEntityService implements OnModuleInit {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
antennaId: In(myAntennas.map(x => x.id)),
read: false,
}) : null;
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
where: {
antennaId: In(myAntennas.map(x => x.id)),
read: false,
},
}) : false);
return unread != null;
return isUnread;
*/
return false; // TODO
}

View File

@ -189,7 +189,11 @@ export class ActivityPubServerService {
return (this.apRendererService.addContext(rendered));
} else {
// index page
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
const rendered = this.apRendererService.renderOrderedCollection(
partOf,
user.followersCount,
`${partOf}?page=true`,
);
reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
@ -277,7 +281,11 @@ export class ActivityPubServerService {
return (this.apRendererService.addContext(rendered));
} else {
// index page
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
const rendered = this.apRendererService.renderOrderedCollection(
partOf,
user.followingCount,
`${partOf}?page=true`,
);
reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered));
@ -310,7 +318,10 @@ export class ActivityPubServerService {
const rendered = this.apRendererService.renderOrderedCollection(
`${this.config.url}/users/${userId}/collections/featured`,
renderedNotes.length, undefined, undefined, renderedNotes,
renderedNotes.length,
undefined,
undefined,
renderedNotes,
);
reply.header('Cache-Control', 'public, max-age=180');
@ -395,7 +406,9 @@ export class ActivityPubServerService {
return (this.apRendererService.addContext(rendered));
} else {
// index page
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
const rendered = this.apRendererService.renderOrderedCollection(
partOf,
user.notesCount,
`${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000`,
);

View File

@ -128,12 +128,12 @@ export class SignupApiService {
}
if (instance.emailRequiredForSignup) {
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
throw new FastifyReplyError(400, 'USED_USERNAME');
}

View File

@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw e;
});
const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id });
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyPromoted);
}

View File

@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) {
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
if (!roleExist) {
throw new ApiError(meta.errors.noSuchRole);
}

View File

@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const accessToken = secureRndstr(32);
// Fetch exist access token
const exist = await this.accessTokensRepository.findOneBy({
appId: session.appId,
userId: me.id,
const exist = await this.accessTokensRepository.exist({
where: {
appId: session.appId,
userId: me.id,
},
});
if (exist == null) {
if (!exist) {
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
// Generate Hash

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// Check if already blocking
const exist = await this.blockingsRepository.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,
const exist = await this.blockingsRepository.exist({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyBlocking);
}

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// Check not blocking
const exist = await this.blockingsRepository.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,
const exist = await this.blockingsRepository.exist({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,
}
});
if (exist == null) {
if (!exist) {
throw new ApiError(meta.errors.notBlocking);
}

View File

@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw e;
});
const exist = await this.clipNotesRepository.findOneBy({
noteId: note.id,
clipId: clip.id,
const exist = await this.clipNotesRepository.exist({
where: {
noteId: note.id,
clipId: clip.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyClipped);
}

View File

@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchClip);
}
const exist = await this.clipFavoritesRepository.findOneBy({
clipId: clip.id,
userId: me.id,
const exist = await this.clipFavoritesRepository.exist({
where: {
clipId: clip.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyFavorited);
}

View File

@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({
md5: ps.md5,
userId: me.id,
const exist = await this.driveFilesRepository.exist({
where: {
md5: ps.md5,
userId: me.id,
},
});
return file != null;
return exist;
});
}
}

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// if already liked
const exist = await this.flashLikesRepository.findOneBy({
flashId: flash.id,
userId: me.id,
const exist = await this.flashLikesRepository.exist({
where: {
flashId: flash.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyLiked);
}

View File

@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// Check if already following
const exist = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
const exist = await this.followingsRepository.exist({
where: {
followerId: follower.id,
followeeId: followee.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyFollowing);
}

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// Check not following
const exist = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
const exist = await this.followingsRepository.exist({
where: {
followerId: follower.id,
followeeId: followee.id,
},
});
if (exist == null) {
if (!exist) {
throw new ApiError(meta.errors.notFollowing);
}

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// if already liked
const exist = await this.galleryLikesRepository.findOneBy({
postId: post.id,
userId: me.id,
const exist = await this.galleryLikesRepository.exist({
where: {
postId: post.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyLiked);
}

View File

@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private downloadService: DownloadService,
) {
super(meta, paramDef, async (ps, me) => {
const users = await this.usersRepository.findOneBy({ id: me.id });
if (users === null) throw new ApiError(meta.errors.noSuchUser);
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file === null) throw new ApiError(meta.errors.noSuchFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);

View File

@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
// Check if announcement exists
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });
const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
if (announcement == null) {
if (!announcementExist) {
throw new ApiError(meta.errors.noSuchAnnouncement);
}
// Check if already read
const read = await this.announcementReadsRepository.findOneBy({
announcementId: ps.announcementId,
userId: me.id,
const alreadyRead = await this.announcementReadsRepository.exist({
where: {
announcementId: ps.announcementId,
userId: me.id,
},
});
if (read != null) {
if (alreadyRead) {
return;
}

View File

@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
if (token) {
if (tokenExist) {
await this.accessTokensRepository.delete({
id: ps.tokenId,
userId: me.id,

View File

@ -79,12 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// Check if already muting
const exist = await this.mutingsRepository.findOneBy({
muterId: muter.id,
muteeId: mutee.id,
const exist = await this.mutingsRepository.exist({
where: {
muterId: muter.id,
muteeId: mutee.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyMuting);
}

View File

@ -217,11 +217,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking
if (renote.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: renote.userId,
blockeeId: me.id,
const blockExist = await this.blockingsRepository.exist({
where: {
blockerId: renote.userId,
blockeeId: me.id,
},
});
if (block) {
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
@ -240,11 +242,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking
if (reply.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: reply.userId,
blockeeId: me.id,
const blockExist = await this.blockingsRepository.exist({
where: {
blockerId: reply.userId,
blockeeId: me.id,
},
});
if (block) {
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}

View File

@ -63,12 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
// if already favorited
const exist = await this.noteFavoritesRepository.findOneBy({
noteId: note.id,
userId: me.id,
const exist = await this.noteFavoritesRepository.exist({
where: {
noteId: note.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyFavorited);
}

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// if already liked
const exist = await this.pageLikesRepository.findOneBy({
pageId: page.id,
userId: me.id,
const exist = await this.pageLikesRepository.exist({
where: {
pageId: page.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
throw new ApiError(meta.errors.alreadyLiked);
}

View File

@ -44,12 +44,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw err;
});
const exist = await this.promoReadsRepository.findOneBy({
noteId: note.id,
userId: me.id,
const exist = await this.promoReadsRepository.exist({
where: {
noteId: note.id,
userId: me.id,
},
});
if (exist != null) {
if (exist) {
return;
}

View File

@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const following = await this.followingsRepository.findOneBy({
followeeId: user.id,
followerId: me.id,
const isFollowing = await this.followingsRepository.exist({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (following == null) {
if (!isFollowing) {
throw new ApiError(meta.errors.forbidden);
}
}

View File

@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const following = await this.followingsRepository.findOneBy({
followeeId: user.id,
followerId: me.id,
const isFollowing = await this.followingsRepository.exist({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (following == null) {
if (!isFollowing) {
throw new ApiError(meta.errors.forbidden);
}
}

View File

@ -84,11 +84,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const list = await this.userListsRepository.findOneBy({
id: ps.listId,
isPublic: true,
const listExist = await this.userListsRepository.exist({
where: {
id: ps.listId,
isPublic: true,
},
});
if (list === null) throw new ApiError(meta.errors.noSuchList);
if (!listExist) throw new ApiError(meta.errors.noSuchList);
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
@ -114,18 +116,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
if (currentUser.id !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: currentUser.id,
blockeeId: me.id,
const blockExist = await this.blockingsRepository.exist({
where: {
blockerId: currentUser.id,
blockeeId: me.id,
},
});
if (block) {
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
const exist = await this.userListJoiningsRepository.findOneBy({
userListId: userList.id,
userId: currentUser.id,
const exist = await this.userListJoiningsRepository.exist({
where: {
userListId: userList.id,
userId: currentUser.id,
},
});
if (exist) {

View File

@ -41,21 +41,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({
id: ps.listId,
isPublic: true,
const userListExist = await this.userListsRepository.exist({
where: {
id: ps.listId,
isPublic: true,
},
});
if (userList === null) {
if (!userListExist) {
throw new ApiError(meta.errors.noSuchList);
}
const exist = await this.userListFavoritesRepository.findOneBy({
userId: me.id,
userListId: ps.listId,
const exist = await this.userListFavoritesRepository.exist({
where: {
userId: me.id,
userListId: ps.listId,
},
});
if (exist !== null) {
if (exist) {
throw new ApiError(meta.errors.alreadyFavorited);
}

View File

@ -100,18 +100,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking
if (user.id !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: user.id,
blockeeId: me.id,
const blockExist = await this.blockingsRepository.exist({
where: {
blockerId: user.id,
blockeeId: me.id,
},
});
if (block) {
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
const exist = await this.userListJoiningsRepository.findOneBy({
userListId: userList.id,
userId: user.id,
const exist = await this.userListJoiningsRepository.exist({
where: {
userListId: userList.id,
userId: user.id,
},
});
if (exist) {

View File

@ -69,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userListId: ps.listId,
});
if (me !== null) {
additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
userId: me.id,
userListId: ps.listId,
}) !== null);
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
where: {
userId: me.id,
userListId: ps.listId,
},
});
} else {
additionalProperties.isLiked = false;
}

View File

@ -39,12 +39,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListFavoritesRepository: UserListFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({
id: ps.listId,
isPublic: true,
const userListExist = await this.userListsRepository.exist({
where: {
id: ps.listId,
isPublic: true,
},
});
if (userList === null) {
if (!userListExist) {
throw new ApiError(meta.errors.noSuchList);
}

View File

@ -34,11 +34,13 @@ class UserListChannel extends Channel {
this.listId = params.listId as string;
// Check existence and owner
const list = await this.userListsRepository.findOneBy({
id: this.listId,
userId: this.user!.id,
const listExist = await this.userListsRepository.exist({
where: {
id: this.listId,
userId: this.user!.id,
},
});
if (!list) return;
if (!listExist) return;
// Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send);

View File

@ -1,5 +1,5 @@
import { writeFile } from 'node:fs/promises';
import * as locales from '../../../locales/index.js';
import locales from '../../../locales/index.js';
await writeFile(
new URL('locale.ts', import.meta.url),

View File

@ -39,7 +39,7 @@
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template>
</MkFolder>
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
<template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
</template>
@ -102,10 +102,6 @@ function openPostForm() {
gap: 12px;
}
.containerCenter {
text-align: center;
}
.fontSerif {
font-family: serif;
}

View File

@ -29,11 +29,11 @@ export const Default = {
const canvas = within(canvasElement);
const a = canvas.getByRole<HTMLAnchorElement>('link');
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
await userEvent.click(a, { button: 2 });
await userEvent.pointer({ keys: '[MouseRight]', target: a });
await tick();
const menu = canvas.getByRole('menu');
await expect(menu).toBeInTheDocument();
await userEvent.click(a, { button: 0 });
await userEvent.click(a);
a.blur();
await tick();
await expect(menu).not.toBeInTheDocument();

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n';
import MkAd from './MkAd.vue';
import { i18n } from '@/i18n';
const common = {
render(args) {
return {
@ -36,6 +36,7 @@ const common = {
const i = buttons[0];
await expect(i).toBeInTheDocument();
await userEvent.click(i);
await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back));
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
@ -49,6 +50,7 @@ const common = {
await expect(back).toBeInTheDocument();
await expect(back).toHaveTextContent(i18n.ts._ad.back);
await userEvent.click(back);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
if (reduce) {
await expect(reduce).not.toBeInTheDocument();
}

View File

@ -88,10 +88,13 @@
<template #label>Special thanks</template>
<div class="_gaps" style="text-align: center;">
<div>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
</div>
<div>
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
</div>
<div>
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
</div>
</div>
</FormSection>
@ -161,6 +164,9 @@ const patronsWithIcon = [{
}, {
name: '清遊あみ',
icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg',
}, {
name: 'Nagi8410',
icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
}];
const patrons = [
@ -256,6 +262,7 @@ const patrons = [
'binvinyl',
'渡志郎',
'ぷーざ',
'越貝鯛丸',
];
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));

View File

@ -18,7 +18,7 @@
<MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton>
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
@ -221,7 +221,7 @@ const setCategoryBulk = async () => {
emojisPaginationComponent.value.reload();
};
const setLisenceBulk = async () => {
const setLicenseBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'License',
});

View File

@ -166,7 +166,7 @@ const menuDef = computed(() => [{
active: currentPage?.route.name === 'import-export',
}, {
icon: 'ti ti-plane',
text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`,
text: `${i18n.ts.accountMigration}`,
to: '/settings/migration',
active: currentPage?.route.name === 'migration',
}, {

View File

@ -1,8 +1,5 @@
<template>
<div class="_gaps_m">
<FormInfo warn>
{{ i18n.ts.thisIsExperimentalFeature }}
</FormInfo>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-plane-arrival"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>

View File

@ -11,6 +11,7 @@ export function createAiScriptEnv(opts) {
USER_NAME: $i ? values.STR($i.name) : values.NULL,
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
CURRENT_URL: values.STR(window.location.href),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
await os.alert({
type: type ? type.value : 'info',

View File

@ -21,6 +21,10 @@ globalThis.addEventListener('activate', ev => {
);
});
function offlineContentHTML(): string {
return `<!doctype html>Offline. Service Worker @${_VERSION_} <button onclick="location.reload()">reload</button>`
}
globalThis.addEventListener('fetch', ev => {
let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@ -34,7 +38,14 @@ globalThis.addEventListener('fetch', ev => {
if (!isHTMLRequest) return;
ev.respondWith(
fetch(ev.request)
.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })),
.catch(() => {
return new Response(offlineContentHTML(), {
status: 200,
headers: {
'content-type': 'text/html',
},
});
}),
);
});