mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-27 04:43:09 +02:00
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>
This commit is contained in:
parent
3c6175d959
commit
4f876c9e8d
7 changed files with 189 additions and 247 deletions
|
@ -10,9 +10,10 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import type { IObject } from '../type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApImageService {
|
export class ApImageService {
|
||||||
|
@ -37,18 +38,22 @@ export class ApImageService {
|
||||||
* Imageを作成します。
|
* Imageを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
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) {
|
if (image.url == null) {
|
||||||
throw new Error('invalid image: url not privided');
|
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)) {
|
if (!checkHttps(image.url)) {
|
||||||
throw new Error('invalid image: unexpected schema of url: ' + 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();
|
const instance = await this.metaService.fetch();
|
||||||
|
|
||||||
let file = await this.driveService.uploadFromUrl({
|
const file = await this.driveService.uploadFromUrl({
|
||||||
url: image.url,
|
url: image.url,
|
||||||
user: actor,
|
user: actor,
|
||||||
uri: image.url,
|
uri: image.url,
|
||||||
sensitive: image.sensitive,
|
sensitive: image.sensitive,
|
||||||
isLink: !instance.cacheRemoteFiles,
|
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を更新する
|
||||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
|
||||||
// URLを更新する
|
return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,7 +84,7 @@ export class ApImageService {
|
||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
|
|
|
@ -22,8 +22,8 @@ export class ApMentionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
|
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
|
||||||
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
|
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
|
||||||
|
|
||||||
const limit = promiseLimit<User | null>(2);
|
const limit = promiseLimit<User | null>(2);
|
||||||
const mentionedUsers = (await Promise.all(
|
const mentionedUsers = (await Promise.all(
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.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 { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
import { ApDbResolverService } from '../ApDbResolverService.js';
|
import { ApDbResolverService } from '../ApDbResolverService.js';
|
||||||
|
@ -72,13 +71,9 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string) {
|
public validateNote(object: IObject, uri: string): Error | null {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
|
|
||||||
if (object == null) {
|
|
||||||
return new Error('invalid Note: object is null');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validPost.includes(getApType(object))) {
|
if (!validPost.includes(getApType(object))) {
|
||||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +105,7 @@ export class ApNoteService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
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();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
@ -117,12 +113,10 @@ export class ApNoteService {
|
||||||
const entryUri = getApId(value);
|
const entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri);
|
const err = this.validateNote(object, entryUri);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(`${err.message}`, {
|
this.logger.error(err.message, {
|
||||||
resolver: {
|
resolver: { history: resolver.getHistory() },
|
||||||
history: resolver.getHistory(),
|
value,
|
||||||
},
|
object,
|
||||||
value: value,
|
|
||||||
object: object,
|
|
||||||
});
|
});
|
||||||
throw new Error('invalid note');
|
throw new Error('invalid note');
|
||||||
}
|
}
|
||||||
|
@ -144,7 +138,11 @@ export class ApNoteService {
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
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) {
|
if (actor.isSuspended) {
|
||||||
|
@ -164,59 +162,49 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
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は必ずしもImageではない
|
||||||
// TODO: attachmentは必ずしも配列ではない
|
// TODO: attachmentは必ずしも配列ではない
|
||||||
// Noteがsensitiveなら添付もsensitiveにする
|
const limit = promiseLimit<DriveFile>(2);
|
||||||
const limit = promiseLimit(2);
|
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
||||||
|
limit(() => this.apImageService.resolveImage(actor, {
|
||||||
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
|
...attach,
|
||||||
const files = note.attachment
|
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
||||||
.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 reply: Note | null = note.inReplyTo
|
const reply: Note | null = note.inReplyTo
|
||||||
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
|
? await this.resolveNote(note.inReplyTo, resolver)
|
||||||
|
.then(x => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
this.logger.warn('Specified inReplyTo, but not found');
|
||||||
throw new Error('inReplyTo not found');
|
throw new Error('inReplyTo not found');
|
||||||
} else {
|
|
||||||
return x;
|
|
||||||
}
|
}
|
||||||
}).catch(async err => {
|
|
||||||
|
return x;
|
||||||
|
})
|
||||||
|
.catch(async err => {
|
||||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
let quote: Note | undefined | null;
|
let quote: Note | undefined | null = null;
|
||||||
|
|
||||||
if (note._misskey_quote || note.quoteUrl) {
|
if (note._misskey_quote || note.quoteUrl) {
|
||||||
const tryResolveNote = async (uri: string): Promise<{
|
const tryResolveNote = async (uri: string): Promise<
|
||||||
status: 'ok';
|
| { status: 'ok'; res: Note }
|
||||||
res: Note | null;
|
| { status: 'permerror' | 'temperror' }
|
||||||
} | {
|
> => {
|
||||||
status: 'permerror' | 'temperror';
|
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
||||||
}> => {
|
|
||||||
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
|
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri);
|
const res = await this.resolveNote(uri);
|
||||||
if (res) {
|
if (res == null) return { status: 'permerror' };
|
||||||
return {
|
return { status: 'ok', res };
|
||||||
status: 'ok',
|
|
||||||
res,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: 'permerror',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
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 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 (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw new Error('quote resolve failed');
|
throw new Error('quote resolve failed');
|
||||||
|
@ -271,7 +259,7 @@ export class ApNoteService {
|
||||||
|
|
||||||
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
||||||
this.logger.info(`extractEmojis: ${e}`);
|
this.logger.info(`extractEmojis: ${e}`);
|
||||||
return [] as Emoji[];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const apEmojis = emojis.map(emoji => emoji.name);
|
const apEmojis = emojis.map(emoji => emoji.name);
|
||||||
|
@ -309,19 +297,18 @@ export class ApNoteService {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
if (uri == null) throw new Error('missing uri');
|
if (uri == null) throw new Error('missing uri');
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしていたら中断
|
||||||
const meta = await this.metaService.fetch();
|
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);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.fetchNote(uri);
|
const exist = await this.fetchNote(uri);
|
||||||
|
if (exist) return exist;
|
||||||
if (exist) {
|
|
||||||
return exist;
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
if (uri.startsWith(this.config.url)) {
|
||||||
|
@ -339,43 +326,41 @@ export class ApNoteService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
|
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
if (!tags) return [];
|
|
||||||
|
|
||||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
const eomjiTags = toArray(tags).filter(isEmoji);
|
||||||
|
|
||||||
const existingEmojis = await this.emojisRepository.findBy({
|
const existingEmojis = await this.emojisRepository.findBy({
|
||||||
host,
|
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 => {
|
return await Promise.all(eomjiTags.map(async tag => {
|
||||||
const name = tag.name!.replaceAll(':', '');
|
const name = tag.name.replaceAll(':', '');
|
||||||
tag.icon = toSingle(tag.icon);
|
tag.icon = toSingle(tag.icon);
|
||||||
|
|
||||||
const exists = existingEmojis.find(x => x.name === name);
|
const exists = existingEmojis.find(x => x.name === name);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
if ((tag.updated != null && exists.updatedAt == null)
|
if ((exists.updatedAt == null)
|
||||||
|| (tag.id != null && exists.uri == null)
|
|| (tag.id != null && exists.uri == null)
|
||||||
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|
|| (new Date(tag.updated) > exists.updatedAt)
|
||||||
|| (tag.icon!.url !== exists.originalUrl)
|
|| (tag.icon.url !== exists.originalUrl)
|
||||||
) {
|
) {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
}, {
|
}, {
|
||||||
uri: tag.id,
|
uri: tag.id,
|
||||||
originalUrl: tag.icon!.url,
|
originalUrl: tag.icon.url,
|
||||||
publicUrl: tag.icon!.url,
|
publicUrl: tag.icon.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.emojisRepository.findOneBy({
|
const emoji = await this.emojisRepository.findOneBy({ host, name });
|
||||||
host,
|
if (emoji == null) throw new Error('emoji update failed');
|
||||||
name,
|
return emoji;
|
||||||
}) as Emoji;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return exists;
|
return exists;
|
||||||
|
@ -388,11 +373,11 @@ export class ApNoteService {
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
uri: tag.id,
|
uri: tag.id,
|
||||||
originalUrl: tag.icon!.url,
|
originalUrl: tag.icon.url,
|
||||||
publicUrl: tag.icon!.url,
|
publicUrl: tag.icon.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
aliases: [],
|
aliases: [],
|
||||||
} as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Config } from '@/config.js';
|
||||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import { User } 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 { Note } from '@/models/entities/Note.js';
|
||||||
import type { IdService } from '@/core/IdService.js';
|
import type { IdService } from '@/core/IdService.js';
|
||||||
import type { MfmService } from '@/core/MfmService.js';
|
import type { MfmService } from '@/core/MfmService.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
|
||||||
import { toArray } from '@/misc/prelude/array.js';
|
import { toArray } from '@/misc/prelude/array.js';
|
||||||
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
|
@ -48,6 +47,8 @@ import type { IActor, IObject } from '../type.js';
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
|
|
||||||
|
type Field = Record<'name' | 'value', string>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApPersonService implements OnModuleInit {
|
export class ApPersonService implements OnModuleInit {
|
||||||
private utilityService: UtilityService;
|
private utilityService: UtilityService;
|
||||||
|
@ -94,28 +95,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: 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.utilityService = this.moduleRef.get('UtilityService');
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||||
|
@ -153,10 +136,6 @@ export class ApPersonService implements OnModuleInit {
|
||||||
private validateActor(x: IObject, uri: string): IActor {
|
private validateActor(x: IObject, uri: string): IActor {
|
||||||
const expectHost = this.punyHost(uri);
|
const expectHost = this.punyHost(uri);
|
||||||
|
|
||||||
if (x == null) {
|
|
||||||
throw new Error('invalid Actor: object is null');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isActor(x)) {
|
if (!isActor(x)) {
|
||||||
throw new Error(`invalid Actor type '${x.type}'`);
|
throw new Error(`invalid Actor type '${x.type}'`);
|
||||||
}
|
}
|
||||||
|
@ -218,21 +197,19 @@ export class ApPersonService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
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 | undefined;
|
||||||
|
|
||||||
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
|
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
const id = uri.split('/').pop();
|
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);
|
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
|
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.cacheService.uriPersonCache.set(uri, 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');
|
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();
|
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);
|
const person = this.validateActor(object, uri);
|
||||||
|
|
||||||
|
@ -264,9 +243,9 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
const host = this.punyHost(object.id);
|
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';
|
const isBot = getApType(object) === 'Service';
|
||||||
|
|
||||||
|
@ -279,7 +258,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
let user: RemoteUser;
|
let user: RemoteUser | null = null;
|
||||||
try {
|
try {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
|
@ -290,16 +269,16 @@ export class ApPersonService implements OnModuleInit {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo,
|
movedToUri: person.movedTo,
|
||||||
movedAt: person.movedTo ? new Date() : null,
|
movedAt: person.movedTo ? new Date() : null,
|
||||||
alsoKnownAs: person.alsoKnownAs,
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
usernameLower: person.preferredUsername?.toLowerCase(),
|
||||||
host,
|
host,
|
||||||
inbox: person.inbox,
|
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,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured ? getApId(person.featured) : undefined,
|
featured: person.featured ? getApId(person.featured) : undefined,
|
||||||
uri: person.id,
|
uri: person.id,
|
||||||
|
@ -311,9 +290,9 @@ export class ApPersonService implements OnModuleInit {
|
||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||||
url: url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
birthday: bday ? bday[0] : null,
|
birthday: bday?.[0] ?? null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
userHost: host,
|
userHost: host,
|
||||||
}));
|
}));
|
||||||
|
@ -330,21 +309,18 @@ export class ApPersonService implements OnModuleInit {
|
||||||
// duplicate key error
|
// duplicate key error
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||||
const u = await this.usersRepository.findOneBy({
|
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
||||||
uri: person.id,
|
if (u == null) throw new Error('already registered');
|
||||||
});
|
|
||||||
|
|
||||||
if (u) {
|
|
||||||
user = u as RemoteUser;
|
user = u as RemoteUser;
|
||||||
} else {
|
|
||||||
throw new Error('already registered');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
this.federatedInstanceService.fetch(host).then(async i => {
|
this.federatedInstanceService.fetch(host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
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 アバターとヘッダー画像をフェッチ
|
//#region アバターとヘッダー画像をフェッチ
|
||||||
const [avatar, banner] = await Promise.all([
|
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||||
person.icon,
|
if (img == null) return null;
|
||||||
person.image,
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
].map(img =>
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
img == null
|
}));
|
||||||
? Promise.resolve(null)
|
|
||||||
: this.apImageService.resolveImage(user!, img).catch(() => null),
|
|
||||||
));
|
|
||||||
|
|
||||||
const avatarId = avatar ? avatar.id : null;
|
const avatarId = avatar?.id ?? null;
|
||||||
const bannerId = banner ? banner.id : null;
|
const bannerId = banner?.id ?? null;
|
||||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||||
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
const avatarBlurhash = avatar?.blurhash ?? null;
|
||||||
const bannerBlurhash = banner ? banner.blurhash : null;
|
const bannerBlurhash = banner?.blurhash ?? null;
|
||||||
|
|
||||||
await this.usersRepository.update(user!.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
avatarId,
|
avatarId,
|
||||||
bannerId,
|
bannerId,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
|
@ -385,30 +358,28 @@ export class ApPersonService implements OnModuleInit {
|
||||||
bannerBlurhash,
|
bannerBlurhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
user!.avatarId = avatarId;
|
user.avatarId = avatarId;
|
||||||
user!.bannerId = bannerId;
|
user.bannerId = bannerId;
|
||||||
user!.avatarUrl = avatarUrl;
|
user.avatarUrl = avatarUrl;
|
||||||
user!.bannerUrl = bannerUrl;
|
user.bannerUrl = bannerUrl;
|
||||||
user!.avatarBlurhash = avatarBlurhash;
|
user.avatarBlurhash = avatarBlurhash;
|
||||||
user!.bannerBlurhash = bannerBlurhash;
|
user.bannerBlurhash = bannerBlurhash;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region カスタム絵文字取得
|
//#region カスタム絵文字取得
|
||||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||||
this.logger.info(`extractEmojis: ${err}`);
|
this.logger.info(`extractEmojis: ${err}`);
|
||||||
return [] as Emoji[];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiNames = emojis.map(emoji => emoji.name);
|
const emojiNames = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
await this.usersRepository.update(user!.id, {
|
await this.usersRepository.update(user.id, { emojis: emojiNames });
|
||||||
emojis: emojiNames,
|
|
||||||
});
|
|
||||||
//#endregion
|
//#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');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) {
|
if (uri.startsWith(`${this.config.url}/`)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||||
|
if (exist === null) return;
|
||||||
if (exist === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = hint ?? await resolver.resolve(uri);
|
const object = hint ?? await resolver.resolve(uri);
|
||||||
|
@ -447,26 +414,22 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.logger.info(`Updating the Person: ${person.id}`);
|
this.logger.info(`Updating the Person: ${person.id}`);
|
||||||
|
|
||||||
// アバターとヘッダー画像をフェッチ
|
// アバターとヘッダー画像をフェッチ
|
||||||
const [avatar, banner] = await Promise.all([
|
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||||
person.icon,
|
if (img == null) return null;
|
||||||
person.image,
|
return this.apImageService.resolveImage(exist, img).catch(() => null);
|
||||||
].map(img =>
|
}));
|
||||||
img == null
|
|
||||||
? Promise.resolve(null)
|
|
||||||
: this.apImageService.resolveImage(exist, img).catch(() => null),
|
|
||||||
));
|
|
||||||
|
|
||||||
// カスタム絵文字取得
|
// カスタム絵文字取得
|
||||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
||||||
this.logger.info(`extractEmojis: ${e}`);
|
this.logger.info(`extractEmojis: ${e}`);
|
||||||
return [] as Emoji[];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiNames = emojis.map(emoji => emoji.name);
|
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}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
||||||
|
@ -479,7 +442,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
const updates = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
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,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
emojis: emojiNames,
|
emojis: emojiNames,
|
||||||
|
@ -487,18 +450,29 @@ export class ApPersonService implements OnModuleInit {
|
||||||
tags,
|
tags,
|
||||||
isBot: getApType(object) === 'Service',
|
isBot: getApType(object) === 'Service',
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} 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();
|
if (moving) updates.movedAt = new Date();
|
||||||
|
|
||||||
|
@ -525,10 +499,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||||
url: url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
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,
|
location: person['vcard:Address'] ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -538,11 +512,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.hashtagService.updateUsertags(exist, tags);
|
this.hashtagService.updateUsertags(exist, tags);
|
||||||
|
|
||||||
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||||
await this.followingsRepository.update({
|
await this.followingsRepository.update(
|
||||||
followerId: exist.id,
|
{ followerId: exist.id },
|
||||||
}, {
|
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
|
||||||
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||||
|
|
||||||
|
@ -580,27 +553,22 @@ export class ApPersonService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.fetchPerson(uri);
|
const exist = await this.fetchPerson(uri);
|
||||||
|
if (exist) return exist;
|
||||||
if (exist) {
|
|
||||||
return exist;
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
return await this.createPerson(uri, resolver);
|
return await this.createPerson(uri, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
|
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
|
||||||
const fields: {
|
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
|
||||||
name: string,
|
const fields: Field[] = [];
|
||||||
value: string
|
|
||||||
}[] = [];
|
|
||||||
if (Array.isArray(attachments)) {
|
if (Array.isArray(attachments)) {
|
||||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||||
fields.push({
|
fields.push({
|
||||||
|
@ -610,11 +578,11 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { fields };
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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 });
|
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
|
@ -643,13 +611,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
// とりあえずidを別の時間で生成して順番を維持
|
// とりあえずidを別の時間で生成して順番を維持
|
||||||
let td = 0;
|
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;
|
td -= 1000;
|
||||||
transactionalEntityManager.insert(UserNotePining, {
|
transactionalEntityManager.insert(UserNotePining, {
|
||||||
id: this.idService.genId(new Date(Date.now() + td)),
|
id: this.idService.genId(new Date(Date.now() + td)),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
noteId: note!.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { IPoll } from '@/models/entities/Poll.js';
|
import type { IPoll } from '@/models/entities/Poll.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isQuestion } from '../type.js';
|
import { isQuestion } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IQuestion } from '../type.js';
|
import type { IObject, IQuestion } from '../type.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApQuestionService {
|
export class ApQuestionService {
|
||||||
|
@ -33,33 +33,25 @@ export class ApQuestionService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const question = await resolver.resolve(source);
|
const question = await resolver.resolve(source);
|
||||||
|
if (!isQuestion(question)) throw new Error('invalid type');
|
||||||
|
|
||||||
if (!isQuestion(question)) {
|
const multiple = question.oneOf === undefined;
|
||||||
throw new Error('invalid type');
|
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;
|
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
|
||||||
|
|
||||||
if (multiple && !question.anyOf) {
|
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||||
throw new Error('invalid question');
|
?.map((x) => x.name)
|
||||||
}
|
.filter((x): x is string => typeof x === 'string')
|
||||||
|
?? [];
|
||||||
|
|
||||||
const choices = question[multiple ? 'anyOf' : 'oneOf']!
|
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
||||||
.map((x, i) => x.name!);
|
|
||||||
|
|
||||||
const votes = question[multiple ? 'anyOf' : 'oneOf']!
|
return { choices, votes, multiple, expiresAt };
|
||||||
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
choices,
|
|
||||||
votes,
|
|
||||||
multiple,
|
|
||||||
expiresAt,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,8 +60,9 @@ export class ApQuestionService {
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
|
if (uri == null) throw new Error('uri is null');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
||||||
|
@ -83,6 +76,7 @@ export class ApQuestionService {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value) as IQuestion;
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
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');
|
if (question.type !== 'Question') throw new Error('object is not a Question');
|
||||||
|
|
||||||
const apChoices = question.oneOf ?? question.anyOf;
|
const apChoices = question.oneOf ?? question.anyOf;
|
||||||
|
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const choice of poll.choices) {
|
for (const choice of poll.choices) {
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
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) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
@ -103,9 +99,7 @@ export class ApQuestionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pollsRepository.update({ noteId: note.id }, {
|
await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes });
|
||||||
votes: poll.votes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
|
||||||
import { isHashtag } from '../type.js';
|
import { isHashtag } from '../type.js';
|
||||||
import type { IObject, IApHashtag } 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 [];
|
if (tags == null) return [];
|
||||||
|
|
||||||
const hashtags = extractApHashtagObjects(tags);
|
const hashtags = extractApHashtagObjects(tags);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function checkHttps(url: string) {
|
export function checkHttps(url: string): boolean {
|
||||||
return url.startsWith('https://') ||
|
return url.startsWith('https://') ||
|
||||||
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
|
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue