summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorokayurisotto <okayurisotto@proton.me>2023-07-08 08:57:13 +0900
committerGitHub <noreply@github.com>2023-07-08 08:57:13 +0900
commit4f876c9e8d8e3cfb64798e84739913708cfaaef5 (patch)
treea8d9597b904060da2968b8e5ea528cd0b9c7de4f
parent広告の曜日を設定できるように (#10095) (diff)
downloadsharkey-4f876c9e8d8e3cfb64798e84739913708cfaaef5.tar.gz
sharkey-4f876c9e8d8e3cfb64798e84739913708cfaaef5.tar.bz2
sharkey-4f876c9e8d8e3cfb64798e84739913708cfaaef5.zip
refactor(backend): `core/activitypub/models` (#11067)
* cleanup(`ApImageService.ts`) * refactor(`ApImageService.ts`) * cleanup(`check-https.ts`) * cleanup(`ApMentionService.ts`) * refactor(`ApMentionService.ts`) * cleanup(`ApNoteService.ts`): unneeded `eslint-disable-next-line` * cleanup(`ApNoteService.ts`) * WIP(`ApImageService.ts`): `image.url`を`getApHrefNullable()`に通すかどうか悩んでいる * refactor(`ApNoteService.ts`): function return type * cleanup(`ApNoteService.ts`): deadcode * refactor(`ApNoteService.ts`): `eslint-disable-next-line` * refactor(`ApNoteService.ts`): non-null assertion これまでは`getApId()`の方でエラーがスローされていた。 * cleanup(`ApNoteService.ts`): unneeded await * refactor(`ApNoteService.ts`): note.attachment - `toArray()`を使うように - よくわからない条件式を整理 - `as`をなくすために`promiseLimit()`でジェネリクスを使うように * cleanup(`ApNoteService.ts`) * refactor(`ApNoteService.ts`): よりよい型定義 `res`が`null`でないことは確認されているようだったので`null`とのunionはなくした * refactor(`ApNoteService.ts`): 不要な条件を削除 * cleanup(`ApNoteService.ts`) * cleanup(`ApNoteService.ts`): 重要でない`as`を削除 * refactor(`ApNoteService.ts`): `eslint-disable-next-line` * cleanup(`ApNoteService.ts`): deadcode * cleanup(`ApNoteService.ts`): unneeded non-null assertion * refactor(`ApNoteService.ts`): 不要な条件を削除 * WIP(`ApNoteService.ts`): `as`をなくす エラーメッセージを考える * cleanup(`ApNoteService.ts`): 不要な`as`を削除 * cleanup(`ApPersonService.ts`): `no-unused-vars` * cleanup(`ApPersonService.ts`): deadcode * refactor(`ApPersonService.ts`): function return type * cleanup(`ApPersonService.ts`): deadcode * cleanup(`ApPersonService.ts`): deadcode * WIP(`ApPersonService.ts`): `as`を調整 `null`でないか確認する処理が続いていたので型アサーションは`null`とのunionにした。 より本質的な改善の余地があるように感じるのでひとまずWIPとしてコミット。 * refactor(`ApPersonService.ts`): `eslint-disable-next-line` * WIP(`ApPersonService.ts`): `as any`をなくした エラーをスローするようにせざるを得なかったのでエラーメッセージを考える必要がある。 * WIP(`ApNoteService.ts`): non-null assertion non-nullアサーションを減らすために事前に存在確認をするようにした。 エラーをスローするようにしたのでメッセージを考えなければならない。 * refactor(`ApNoteService.ts`): non-null assertion -> optional chaining * refactor(`ApPersonService.ts`): `eslint-disable-next-line` * refactor(`ApPersonService.ts`): `eslint-disable-next-line` * refactor(`ApPersonService.ts`): function return type * refactor(`ApPersonService.ts`): type guardによるnon-null assertionの削除 * WIP(`ApPersonService.ts`): `analyzeAttachments` - Field型を事前に定義しておくように - `attachments`が`IObject`だった場合、返り値が`{ fields: [] }`になるようだが構わないのか? - `toArray()`を通すべきでは? * Revert "WIP(`ApImageService.ts`): `image.url`を`getApHrefNullable()`に通すかどうか悩んでいる" This reverts commit aeefb843a8a688f8a356794e8981c58f8a2733af. * cleanup(`ApImageService.ts`): `import` * refactor(`ApImageService.ts`): 冗長だった部分を短く * cleanup(`ApMentionService.ts`): `import` * refactor(`ApImageService.ts`): `JSON.stringify()`でのindentationを追加 * cleanup(`ApNoteService.ts`): `import` * cleanup(`ApNoteService.ts`) * cleanup(`ApNoteService.ts`) * cleanup(`ApNoteService.ts`) * cleanup(`ApNoteService.ts`): `any`に対するnon-null assertion * refactor(`ApNoteService.ts`): 添付ファイル * cleanup(`ApPersonService.ts`): `import` * refactor(`ApPersonService.ts`): より実情に即した`as`に * cleanup(`ApPersonService.ts`) * refactor(`ApPersonService.ts`): 冗長だった部分を修正 * cleanup(`ApPersonService.ts`): deadcode * cleanup(`ApPersonService.ts`) * cleanup(`ApQuestionService.ts`): `import` * refactor(`ApQuestionService.ts`): `eslint-disable-next-line` * refactor(`ApQuestionService.ts`): `eslint-disable-next-line` * cleanup(`ApQuestionService.ts`) * refactor(`ApQuestionService.ts`): non-null assertionを消した * cleanup(`ApQuestionService.ts`) * WIP(`ApQuestionService.ts`): non-null assertionを消す エラーメッセージを考える必要がある。 * refactor(`ApQuestionService.ts`): `any`を消す * refactor(`ApQuestionService.ts`): function return type * WIP(`ApPersonService.ts`): 可読性の低い三項演算子を削除しつつnon-null assertionを回避 エラーメッセージを考える必要がある。 * cleanup(`ApPersonService.ts`): 不必要な三項演算子を削除 * cleanup(`ApPersonService.ts`): 不要な`as` * cleanup(`ApPersonService.ts`) * refactor(`ApPersonService.ts`) * refactor(`ApPersonService.ts`): 可読性の低い三項演算子を削除 元の実装が悪いと判断し`null`かどうかの確認をより厳密に行うようにした。 * cleanup(`ApPersonService.ts`) * cleanup(`ApPersonService.ts`) * refactor(`ApPersonService.ts`): 返り値を`void`に統一 この返り値を参照しているコードは見当たらなかった。 また、普通に意味がない値であるように見受けられた。 * fixup! refactor(`ApPersonService.ts`): 返り値を`void`に統一 * refactor(`ApNoteService.ts`) * refactor(`ApPersonService.ts`) * cleanup(`ApPersonService.ts`) * cleanup(`ApPersonService.ts`) * refactor(`ApPersonService.ts`): 返り値の`void`統一と条件式の調整 この返り値を参照しているコードは見当たらなかった。 また、普通に意味がない値であるように見受けられた。 * cleanup(`ApQuestionService.ts`) * refactor(`ApQuestionService.ts`) * refactor(`ApQuestionService.ts`) * refactor(`tag.ts`): function return type * fixup! enhance: account migration (#10592) * fixup! WIP(`ApPersonService.ts`): 可読性の低い三項演算子を削除しつつnon-null assertionを回避 * fixup! cleanup(`ApPersonService.ts`): 不要な`as` * refactor: エラーメッセージを見繕った * Revert "cleanup(`ApImageService.ts`): `import`" This reverts commit 1454d04c377eaf46013b0f3c3ce664a4034fd53a. * Revert "cleanup(`ApMentionService.ts`): `import`" This reverts commit 244f6720c134a3434e33c1caf6e3e0c2c87b58f5. * Revert "cleanup(`ApNoteService.ts`): `import`" This reverts commit d8f0d769733c4cb0629821b04e557a0ae6f5ff5b. * Revert "cleanup(`ApPersonService.ts`): `import`" This reverts commit 5190ef954caf376da46c707f52e02208d53caafd. # Conflicts: # packages/backend/src/core/activitypub/models/ApPersonService.ts * Revert "cleanup(`ApQuestionService.ts`): `import`" This reverts commit 778585e2882477fec5f11fabf398b4b89cf26da2. * processRemoteMoveはそのままにしてほしい * Revert "fixup! refactor(`ApPersonService.ts`): 返り値を`void`に統一" This reverts commit 083cd678abcd64325b9628895366c03b893e42ca. * Revert "refactor(`ApPersonService.ts`): 返り値を`void`に統一" This reverts commit bfa0fcd6f01a6e519ea0c68017358f9980d2ed96. --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp>
-rw-r--r--packages/backend/src/core/activitypub/models/ApImageService.ts35
-rw-r--r--packages/backend/src/core/activitypub/models/ApMentionService.ts4
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts137
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts210
-rw-r--r--packages/backend/src/core/activitypub/models/ApQuestionService.ts42
-rw-r--r--packages/backend/src/core/activitypub/models/tag.ts2
-rw-r--r--packages/backend/src/misc/check-https.ts6
7 files changed, 189 insertions, 247 deletions
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 79c5ae958a..0da312241f 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -10,9 +10,10 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
-import { checkHttps } from '@/misc/check-https.js';
+import type { IObject } from '../type.js';
@Injectable()
export class ApImageService {
@@ -37,18 +38,22 @@ export class ApImageService {
* Imageを作成します。
*/
@bindThis
- public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
+ public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
}
- const image = await this.apResolverService.createResolver().resolve(value) as any;
+ const image = await this.apResolverService.createResolver().resolve(value);
if (image.url == null) {
throw new Error('invalid image: url not privided');
}
+ if (typeof image.url !== 'string') {
+ throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
+ }
+
if (!checkHttps(image.url)) {
throw new Error('invalid image: unexpected schema of url: ' + image.url);
}
@@ -57,29 +62,19 @@ export class ApImageService {
const instance = await this.metaService.fetch();
- let file = await this.driveService.uploadFromUrl({
+ const file = await this.driveService.uploadFromUrl({
url: image.url,
user: actor,
uri: image.url,
sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles,
- comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
+ comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
});
+ if (!file.isLink || file.url === image.url) return file;
- if (file.isLink) {
- // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
- // URLを更新する
- if (file.url !== image.url) {
- await this.driveFilesRepository.update({ id: file.id }, {
- url: image.url,
- uri: image.url,
- });
-
- file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
- }
- }
-
- return file;
+ // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する
+ await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
+ return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
}
/**
@@ -89,7 +84,7 @@ export class ApImageService {
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
@bindThis
- public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
+ public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
// TODO
// リモートサーバーからフェッチしてきて登録
diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts
index 9beefc8fcb..62ae3cf93d 100644
--- a/packages/backend/src/core/activitypub/models/ApMentionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts
@@ -22,8 +22,8 @@ export class ApMentionService {
}
@bindThis
- public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
- const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
+ public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
+ const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all(
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 60a3e56800..35865a819d 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js';
@@ -72,13 +71,9 @@ export class ApNoteService {
}
@bindThis
- public validateNote(object: IObject, uri: string) {
+ public validateNote(object: IObject, uri: string): Error | null {
const expectHost = this.utilityService.extractDbHost(uri);
- if (object == null) {
- return new Error('invalid Note: object is null');
- }
-
if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
}
@@ -110,6 +105,7 @@ export class ApNoteService {
*/
@bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value);
@@ -117,12 +113,10 @@ export class ApNoteService {
const entryUri = getApId(value);
const err = this.validateNote(object, entryUri);
if (err) {
- this.logger.error(`${err.message}`, {
- resolver: {
- history: resolver.getHistory(),
- },
- value: value,
- object: object,
+ this.logger.error(err.message, {
+ resolver: { history: resolver.getHistory() },
+ value,
+ object,
});
throw new Error('invalid note');
}
@@ -144,7 +138,11 @@ export class ApNoteService {
this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
- const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
+ if (note.attributedTo == null) {
+ throw new Error('invalid note.attributedTo: ' + note.attributedTo);
+ }
+
+ const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
@@ -164,59 +162,49 @@ export class ApNoteService {
}
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
- const apHashtags = await extractApHashtags(note.tag);
+ const apHashtags = extractApHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
- // Noteがsensitiveなら添付もsensitiveにする
- const limit = promiseLimit(2);
-
- note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
- const files = note.attachment
- .map(attach => attach.sensitive = note.sensitive)
- ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
- .filter(image => image != null)
- : [];
+ const limit = promiseLimit<DriveFile>(2);
+ const files = (await Promise.all(toArray(note.attachment).map(attach => (
+ limit(() => this.apImageService.resolveImage(actor, {
+ ...attach,
+ sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
+ }))
+ ))));
// リプライ
const reply: Note | null = note.inReplyTo
- ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
- if (x == null) {
- this.logger.warn('Specified inReplyTo, but not found');
- throw new Error('inReplyTo not found');
- } else {
+ ? await this.resolveNote(note.inReplyTo, resolver)
+ .then(x => {
+ if (x == null) {
+ this.logger.warn('Specified inReplyTo, but not found');
+ throw new Error('inReplyTo not found');
+ }
+
return x;
- }
- }).catch(async err => {
- this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
- throw err;
- })
+ })
+ .catch(async err => {
+ this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
+ throw err;
+ })
: null;
// 引用
- let quote: Note | undefined | null;
+ let quote: Note | undefined | null = null;
if (note._misskey_quote || note.quoteUrl) {
- const tryResolveNote = async (uri: string): Promise<{
- status: 'ok';
- res: Note | null;
- } | {
- status: 'permerror' | 'temperror';
- }> => {
- if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
+ const tryResolveNote = async (uri: string): Promise<
+ | { status: 'ok'; res: Note }
+ | { status: 'permerror' | 'temperror' }
+ > => {
+ if (!uri.match(/^https?:/)) return { status: 'permerror' };
try {
const res = await this.resolveNote(uri);
- if (res) {
- return {
- status: 'ok',
- res,
- };
- } else {
- return {
- status: 'permerror',
- };
- }
+ if (res == null) return { status: 'permerror' };
+ return { status: 'ok', res };
} catch (e) {
return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
@@ -225,9 +213,9 @@ export class ApNoteService {
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
- const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
+ const results = await Promise.all(uris.map(tryResolveNote));
- quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
+ quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0);
if (!quote) {
if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed');
@@ -271,7 +259,7 @@ export class ApNoteService {
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
- return [] as Emoji[];
+ return [];
});
const apEmojis = emojis.map(emoji => emoji.name);
@@ -309,19 +297,18 @@ export class ApNoteService {
const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri');
- // ブロックしてたら中断
+ // ブロックしていたら中断
const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
+ if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
+ throw new StatusError('blocked host', 451);
+ }
const unlock = await this.appLockService.getApLock(uri);
try {
//#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri);
-
- if (exist) {
- return exist;
- }
+ if (exist) return exist;
//#endregion
if (uri.startsWith(this.config.url)) {
@@ -339,43 +326,41 @@ export class ApNoteService {
@bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
+ // eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host);
- if (!tags) return [];
-
const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({
host,
- name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
+ name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))),
});
return await Promise.all(eomjiTags.map(async tag => {
- const name = tag.name!.replaceAll(':', '');
+ const name = tag.name.replaceAll(':', '');
tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name);
if (exists) {
- if ((tag.updated != null && exists.updatedAt == null)
+ if ((exists.updatedAt == null)
|| (tag.id != null && exists.uri == null)
- || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
- || (tag.icon!.url !== exists.originalUrl)
+ || (new Date(tag.updated) > exists.updatedAt)
+ || (tag.icon.url !== exists.originalUrl)
) {
await this.emojisRepository.update({
host,
name,
}, {
uri: tag.id,
- originalUrl: tag.icon!.url,
- publicUrl: tag.icon!.url,
+ originalUrl: tag.icon.url,
+ publicUrl: tag.icon.url,
updatedAt: new Date(),
});
- return await this.emojisRepository.findOneBy({
- host,
- name,
- }) as Emoji;
+ const emoji = await this.emojisRepository.findOneBy({ host, name });
+ if (emoji == null) throw new Error('emoji update failed');
+ return emoji;
}
return exists;
@@ -388,11 +373,11 @@ export class ApNoteService {
host,
name,
uri: tag.id,
- originalUrl: tag.icon!.url,
- publicUrl: tag.icon!.url,
+ originalUrl: tag.icon.url,
+ publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
- } as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
}));
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index afe89e5a11..e8b65b3d42 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
-import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
+import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
@@ -15,7 +15,6 @@ import type Logger from '@/logger.js';
import type { Note } from '@/models/entities/Note.js';
import type { IdService } from '@/core/IdService.js';
import type { MfmService } from '@/core/MfmService.js';
-import type { Emoji } from '@/models/entities/Emoji.js';
import { toArray } from '@/misc/prelude/array.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -48,6 +47,8 @@ import type { IActor, IObject } from '../type.js';
const nameLength = 128;
const summaryLength = 2048;
+type Field = Record<'name' | 'value', string>;
+
@Injectable()
export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService;
@@ -94,28 +95,10 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
-
- //private utilityService: UtilityService,
- //private userEntityService: UserEntityService,
- //private idService: IdService,
- //private globalEventService: GlobalEventService,
- //private metaService: MetaService,
- //private federatedInstanceService: FederatedInstanceService,
- //private fetchInstanceMetadataService: FetchInstanceMetadataService,
- //private cacheService: CacheService,
- //private apResolverService: ApResolverService,
- //private apNoteService: ApNoteService,
- //private apImageService: ApImageService,
- //private apMfmService: ApMfmService,
- //private mfmService: MfmService,
- //private hashtagService: HashtagService,
- //private usersChart: UsersChart,
- //private instanceChart: InstanceChart,
- //private apLoggerService: ApLoggerService,
) {
}
- onModuleInit() {
+ onModuleInit(): void {
this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
@@ -153,10 +136,6 @@ export class ApPersonService implements OnModuleInit {
private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri);
- if (x == null) {
- throw new Error('invalid Actor: object is null');
- }
-
if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`);
}
@@ -218,21 +197,19 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
- const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
+ const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined;
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop();
- const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
+ const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null;
if (u) this.cacheService.uriPersonCache.set(uri, u);
return u;
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
+ const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
if (exist) {
this.cacheService.uriPersonCache.set(uri, exist);
@@ -254,9 +231,11 @@ export class ApPersonService implements OnModuleInit {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
}
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
- const object = await resolver.resolve(uri) as any;
+ const object = await resolver.resolve(uri);
+ if (object.id == null) throw new Error('invalid object.id: ' + object.id);
const person = this.validateActor(object, uri);
@@ -264,9 +243,9 @@ export class ApPersonService implements OnModuleInit {
const host = this.punyHost(object.id);
- const { fields } = this.analyzeAttachments(person.attachment ?? []);
+ const fields = this.analyzeAttachments(person.attachment ?? []);
- const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
+ const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const isBot = getApType(object) === 'Service';
@@ -279,7 +258,7 @@ export class ApPersonService implements OnModuleInit {
}
// Create user
- let user: RemoteUser;
+ let user: RemoteUser | null = null;
try {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
@@ -290,16 +269,16 @@ export class ApPersonService implements OnModuleInit {
createdAt: new Date(),
lastFetchedAt: new Date(),
name: truncate(person.name, nameLength),
- isLocked: !!person.manuallyApprovesFollowers,
+ isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs,
- isExplorable: !!person.discoverable,
+ isExplorable: person.discoverable,
username: person.preferredUsername,
- usernameLower: person.preferredUsername!.toLowerCase(),
+ usernameLower: person.preferredUsername?.toLowerCase(),
host,
inbox: person.inbox,
- sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
+ sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id,
@@ -311,9 +290,9 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
- url: url,
+ url,
fields,
- birthday: bday ? bday[0] : null,
+ birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
userHost: host,
}));
@@ -330,21 +309,18 @@ export class ApPersonService implements OnModuleInit {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
- const u = await this.usersRepository.findOneBy({
- uri: person.id,
- });
+ const u = await this.usersRepository.findOneBy({ uri: person.id });
+ if (u == null) throw new Error('already registered');
- if (u) {
- user = u as RemoteUser;
- } else {
- throw new Error('already registered');
- }
+ user = u as RemoteUser;
} else {
this.logger.error(e instanceof Error ? e : new Error(e as string));
throw e;
}
}
+ if (user == null) throw new Error('failed to create user: user is null');
+
// Register host
this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
@@ -354,29 +330,26 @@ export class ApPersonService implements OnModuleInit {
}
});
- this.usersChart.update(user!, true);
+ this.usersChart.update(user, true);
// ハッシュタグ更新
- this.hashtagService.updateUsertags(user!, tags);
+ this.hashtagService.updateUsertags(user, tags);
//#region アバターとヘッダー画像をフェッチ
- const [avatar, banner] = await Promise.all([
- person.icon,
- person.image,
- ].map(img =>
- img == null
- ? Promise.resolve(null)
- : this.apImageService.resolveImage(user!, img).catch(() => null),
- ));
+ const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
+ if (img == null) return null;
+ if (user == null) throw new Error('failed to create user: user is null');
+ return this.apImageService.resolveImage(user, img).catch(() => null);
+ }));
- const avatarId = avatar ? avatar.id : null;
- const bannerId = banner ? banner.id : null;
+ const avatarId = avatar?.id ?? null;
+ const bannerId = banner?.id ?? null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
- const avatarBlurhash = avatar ? avatar.blurhash : null;
- const bannerBlurhash = banner ? banner.blurhash : null;
+ const avatarBlurhash = avatar?.blurhash ?? null;
+ const bannerBlurhash = banner?.blurhash ?? null;
- await this.usersRepository.update(user!.id, {
+ await this.usersRepository.update(user.id, {
avatarId,
bannerId,
avatarUrl,
@@ -385,30 +358,28 @@ export class ApPersonService implements OnModuleInit {
bannerBlurhash,
});
- user!.avatarId = avatarId;
- user!.bannerId = bannerId;
- user!.avatarUrl = avatarUrl;
- user!.bannerUrl = bannerUrl;
- user!.avatarBlurhash = avatarBlurhash;
- user!.bannerBlurhash = bannerBlurhash;
+ user.avatarId = avatarId;
+ user.bannerId = bannerId;
+ user.avatarUrl = avatarUrl;
+ user.bannerUrl = bannerUrl;
+ user.avatarBlurhash = avatarBlurhash;
+ user.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
- return [] as Emoji[];
+ return [];
});
const emojiNames = emojis.map(emoji => emoji.name);
- await this.usersRepository.update(user!.id, {
- emojis: emojiNames,
- });
+ await this.usersRepository.update(user.id, { emojis: emojiNames });
//#endregion
- await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
+ await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
- return user!;
+ return user;
}
/**
@@ -426,18 +397,14 @@ export class ApPersonService implements OnModuleInit {
if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ
- if (uri.startsWith(`${this.config.url}/`)) {
- return;
- }
+ if (uri.startsWith(`${this.config.url}/`)) return;
//#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
-
- if (exist === null) {
- return;
- }
+ if (exist === null) return;
//#endregion
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri);
@@ -447,26 +414,22 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ
- const [avatar, banner] = await Promise.all([
- person.icon,
- person.image,
- ].map(img =>
- img == null
- ? Promise.resolve(null)
- : this.apImageService.resolveImage(exist, img).catch(() => null),
- ));
+ const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
+ if (img == null) return null;
+ return this.apImageService.resolveImage(exist, img).catch(() => null);
+ }));
// カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
- return [] as Emoji[];
+ return [];
});
const emojiNames = emojis.map(emoji => emoji.name);
- const { fields } = this.analyzeAttachments(person.attachment ?? []);
+ const fields = this.analyzeAttachments(person.attachment ?? []);
- const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
+ const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -479,7 +442,7 @@ export class ApPersonService implements OnModuleInit {
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
- sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
+ sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured,
emojis: emojiNames,
@@ -487,18 +450,29 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true,
- isLocked: !!person.manuallyApprovesFollowers,
+ isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
- isExplorable: !!person.discoverable,
+ isExplorable: person.discoverable,
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
- const moving =
+ const moving = ((): boolean => {
// 移行先がない→ある
- (!exist.movedToUri && updates.movedToUri) ||
+ if (
+ exist.movedToUri === null &&
+ updates.movedToUri
+ ) return true;
+
// 移行先がある→別のもの
- (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
+ if (
+ exist.movedToUri !== null &&
+ updates.movedToUri !== null &&
+ exist.movedToUri !== updates.movedToUri
+ ) return true;
+
// 移行先がある→ない、ない→ないは無視
+ return false;
+ })();
if (moving) updates.movedAt = new Date();
@@ -525,10 +499,10 @@ export class ApPersonService implements OnModuleInit {
}
await this.userProfilesRepository.update({ userId: exist.id }, {
- url: url,
+ url,
fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
- birthday: bday ? bday[0] : null,
+ birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
});
@@ -538,11 +512,10 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
- await this.followingsRepository.update({
- followerId: exist.id,
- }, {
- followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
- });
+ await this.followingsRepository.update(
+ { followerId: exist.id },
+ { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
+ );
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
@@ -580,27 +553,22 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
//#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri);
-
- if (exist) {
- return exist;
- }
+ if (exist) return exist;
//#endregion
// リモートサーバーからフェッチしてきて登録
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
return await this.createPerson(uri, resolver);
}
@bindThis
- public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
- const fields: {
- name: string,
- value: string
- }[] = [];
+ // TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
+ public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
+ const fields: Field[] = [];
+
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
fields.push({
@@ -610,11 +578,11 @@ export class ApPersonService implements OnModuleInit {
}
}
- return { fields };
+ return fields;
}
@bindThis
- public async updateFeatured(userId: User['id'], resolver?: Resolver) {
+ public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return;
@@ -643,13 +611,13 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持
let td = 0;
- for (const note of featuredNotes.filter(note => note != null)) {
+ for (const note of featuredNotes.filter((note): note is Note => note != null)) {
td -= 1000;
transactionalEntityManager.insert(UserNotePining, {
id: this.idService.genId(new Date(Date.now() + td)),
createdAt: new Date(),
userId: user.id,
- noteId: note!.id,
+ noteId: note.id,
});
}
});
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index 13a2f0fa5c..229a44f90f 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { IPoll } from '@/models/entities/Poll.js';
import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IQuestion } from '../type.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ApQuestionService {
@@ -33,33 +33,25 @@ export class ApQuestionService {
@bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(source);
+ if (!isQuestion(question)) throw new Error('invalid type');
- if (!isQuestion(question)) {
- throw new Error('invalid type');
- }
+ const multiple = question.oneOf === undefined;
+ if (multiple && question.anyOf === undefined) throw new Error('invalid question');
- const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
- if (multiple && !question.anyOf) {
- throw new Error('invalid question');
- }
-
- const choices = question[multiple ? 'anyOf' : 'oneOf']!
- .map((x, i) => x.name!);
+ const choices = question[multiple ? 'anyOf' : 'oneOf']
+ ?.map((x) => x.name)
+ .filter((x): x is string => typeof x === 'string')
+ ?? [];
- const votes = question[multiple ? 'anyOf' : 'oneOf']!
- .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
+ const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
- return {
- choices,
- votes,
- multiple,
- expiresAt,
- };
+ return { choices, votes, multiple, expiresAt };
}
/**
@@ -68,8 +60,9 @@ export class ApQuestionService {
* @returns true if updated
*/
@bindThis
- public async updateQuestion(value: any, resolver?: Resolver) {
+ public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
const uri = typeof value === 'string' ? value : value.id;
+ if (uri == null) throw new Error('uri is null');
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
@@ -83,6 +76,7 @@ export class ApQuestionService {
//#endregion
// resolve new Question object
+ // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(value) as IQuestion;
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
@@ -90,12 +84,14 @@ export class ApQuestionService {
if (question.type !== 'Question') throw new Error('object is not a Question');
const apChoices = question.oneOf ?? question.anyOf;
+ if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
let changed = false;
for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)];
- const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
+ const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
+ if (newCount == null) throw new Error('invalid newCount: ' + newCount);
if (oldCount !== newCount) {
changed = true;
@@ -103,9 +99,7 @@ export class ApQuestionService {
}
}
- await this.pollsRepository.update({ noteId: note.id }, {
- votes: poll.votes,
- });
+ await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes });
return changed;
}
diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts
index 803846a0b0..9aeb843562 100644
--- a/packages/backend/src/core/activitypub/models/tag.ts
+++ b/packages/backend/src/core/activitypub/models/tag.ts
@@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js';
-export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
+export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] {
if (tags == null) return [];
const hashtags = extractApHashtagObjects(tags);
diff --git a/packages/backend/src/misc/check-https.ts b/packages/backend/src/misc/check-https.ts
index b33f019973..612032fe97 100644
--- a/packages/backend/src/misc/check-https.ts
+++ b/packages/backend/src/misc/check-https.ts
@@ -1,4 +1,4 @@
-export function checkHttps(url: string) {
- return url.startsWith('https://') ||
- (url.startsWith('http://') && process.env.NODE_ENV !== 'production');
+export function checkHttps(url: string): boolean {
+ return url.startsWith('https://') ||
+ (url.startsWith('http://') && process.env.NODE_ENV !== 'production');
}