summaryrefslogtreecommitdiff
path: root/src/remote/activitypub/models
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
commit0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch)
tree40874799472fa07416f17b50a398ac33b7771905 /src/remote/activitypub/models
parentupdate deps (diff)
downloadsharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz
sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2
sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip
refactoring
Resolve #7779
Diffstat (limited to 'src/remote/activitypub/models')
-rw-r--r--src/remote/activitypub/models/icon.ts5
-rw-r--r--src/remote/activitypub/models/identifier.ts5
-rw-r--r--src/remote/activitypub/models/image.ts62
-rw-r--r--src/remote/activitypub/models/mention.ts24
-rw-r--r--src/remote/activitypub/models/note.ts356
-rw-r--r--src/remote/activitypub/models/person.ts494
-rw-r--r--src/remote/activitypub/models/question.ts83
-rw-r--r--src/remote/activitypub/models/tag.ts18
8 files changed, 0 insertions, 1047 deletions
diff --git a/src/remote/activitypub/models/icon.ts b/src/remote/activitypub/models/icon.ts
deleted file mode 100644
index 50794a937d..0000000000
--- a/src/remote/activitypub/models/icon.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type IIcon = {
- type: string;
- mediaType?: string;
- url?: string;
-};
diff --git a/src/remote/activitypub/models/identifier.ts b/src/remote/activitypub/models/identifier.ts
deleted file mode 100644
index f6c3bb8c88..0000000000
--- a/src/remote/activitypub/models/identifier.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type IIdentifier = {
- type: string;
- name: string;
- value: string;
-};
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
deleted file mode 100644
index d0a96e4313..0000000000
--- a/src/remote/activitypub/models/image.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import uploadFromUrl from '@/services/drive/upload-from-url';
-import { IRemoteUser } from '@/models/entities/user';
-import Resolver from '../resolver';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { apLogger } from '../logger';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-import { truncate } from '@/misc/truncate';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
-
-const logger = apLogger;
-
-/**
- * Imageを作成します。
- */
-export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
- // 投稿者が凍結されていたらスキップ
- if (actor.isSuspended) {
- throw new Error('actor has been suspended');
- }
-
- const image = await new Resolver().resolve(value) as any;
-
- if (image.url == null) {
- throw new Error('invalid image: url not privided');
- }
-
- logger.info(`Creating the Image: ${image.url}`);
-
- const instance = await fetchMeta();
- const cache = instance.cacheRemoteFiles;
-
- let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
-
- if (file.isLink) {
- // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
- // URLを更新する
- if (file.url !== image.url) {
- await DriveFiles.update({ id: file.id }, {
- url: image.url,
- uri: image.url
- });
-
- file = await DriveFiles.findOneOrFail(file.id);
- }
- }
-
- return file;
-}
-
-/**
- * Imageを解決します。
- *
- * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
- // TODO
-
- // リモートサーバーからフェッチしてきて登録
- return await createImage(actor, value);
-}
diff --git a/src/remote/activitypub/models/mention.ts b/src/remote/activitypub/models/mention.ts
deleted file mode 100644
index ade9c90806..0000000000
--- a/src/remote/activitypub/models/mention.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { toArray, unique } from '@/prelude/array';
-import { IObject, isMention, IApMention } from '../type';
-import { resolvePerson } from './person';
-import * as promiseLimit from 'promise-limit';
-import Resolver from '../resolver';
-import { User } from '@/models/entities/user';
-
-export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
- const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
-
- const resolver = new Resolver();
-
- const limit = promiseLimit<User | null>(2);
- const mentionedUsers = (await Promise.all(
- hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
- )).filter((x): x is User => x != null);
-
- return mentionedUsers;
-}
-
-export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
- if (tags == null) return [];
- return toArray(tags).filter(isMention);
-}
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
deleted file mode 100644
index 492dc05248..0000000000
--- a/src/remote/activitypub/models/note.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-import * as promiseLimit from 'promise-limit';
-
-import config from '@/config/index';
-import Resolver from '../resolver';
-import post from '@/services/note/create';
-import { resolvePerson, updatePerson } from './person';
-import { resolveImage } from './image';
-import { IRemoteUser } from '@/models/entities/user';
-import { htmlToMfm } from '../misc/html-to-mfm';
-import { extractApHashtags } from './tag';
-import { unique, toArray, toSingle } from '@/prelude/array';
-import { extractPollFromQuestion } from './question';
-import vote from '@/services/note/polls/vote';
-import { apLogger } from '../logger';
-import { DriveFile } from '@/models/entities/drive-file';
-import { deliverQuestionUpdate } from '@/services/note/polls/update';
-import { extractDbHost, toPuny } from '@/misc/convert-host';
-import { Emojis, Polls, MessagingMessages } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type';
-import { Emoji } from '@/models/entities/emoji';
-import { genId } from '@/misc/gen-id';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { getApLock } from '@/misc/app-lock';
-import { createMessage } from '@/services/messages/create';
-import { parseAudience } from '../audience';
-import { extractApMentions } from './mention';
-import DbResolver from '../db-resolver';
-import { StatusError } from '@/misc/fetch';
-
-const logger = apLogger;
-
-export function validateNote(object: any, uri: string) {
- const expectHost = 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)}`);
- }
-
- if (object.id && extractDbHost(object.id) !== expectHost) {
- return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`);
- }
-
- if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`);
- }
-
- return null;
-}
-
-/**
- * Noteをフェッチします。
- *
- * Misskeyに対象のNoteが登録されていればそれを返します。
- */
-export async function fetchNote(object: string | IObject): Promise<Note | null> {
- const dbResolver = new DbResolver();
- return await dbResolver.getNoteFromApId(object);
-}
-
-/**
- * Noteを作成します。
- */
-export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
- if (resolver == null) resolver = new Resolver();
-
- const object: any = await resolver.resolve(value);
-
- const entryUri = getApId(value);
- const err = validateNote(object, entryUri);
- if (err) {
- logger.error(`${err.message}`, {
- resolver: {
- history: resolver.getHistory()
- },
- value: value,
- object: object
- });
- throw new Error('invalid note');
- }
-
- const note: IPost = object;
-
- logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
-
- logger.info(`Creating the Note: ${note.id}`);
-
- // 投稿者をフェッチ
- const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
-
- // 投稿者が凍結されていたらスキップ
- if (actor.isSuspended) {
- throw new Error('actor has been suspended');
- }
-
- const noteAudience = await parseAudience(actor, note.to, note.cc);
- let visibility = noteAudience.visibility;
- const visibleUsers = noteAudience.visibleUsers;
-
- // Audience (to, cc) が指定されてなかった場合
- if (visibility === 'specified' && visibleUsers.length === 0) {
- if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
- // こちらから匿名GET出来たものならばpublic
- visibility = 'public';
- }
- }
-
- let isTalk = note._misskey_talk && visibility === 'specified';
-
- const apMentions = await extractApMentions(note.tag);
- const apHashtags = await 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(() => resolveImage(actor, x)) as Promise<DriveFile>)))
- .filter(image => image != null)
- : [];
-
- // リプライ
- const reply: Note | null = note.inReplyTo
- ? await resolveNote(note.inReplyTo, resolver).then(x => {
- if (x == null) {
- logger.warn(`Specified inReplyTo, but nout found`);
- throw new Error('inReplyTo not found');
- } else {
- return x;
- }
- }).catch(async e => {
- // トークだったらinReplyToのエラーは無視
- const uri = getApId(note.inReplyTo);
- if (uri.startsWith(config.url + '/')) {
- const id = uri.split('/').pop();
- const talk = await MessagingMessages.findOne(id);
- if (talk) {
- isTalk = true;
- return null;
- }
- }
-
- logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
- throw e;
- })
- : null;
-
- // 引用
- let quote: Note | undefined | 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' };
- try {
- const res = await resolveNote(uri);
- if (res) {
- return {
- status: 'ok',
- res
- };
- } else {
- return {
- status: 'permerror'
- };
- }
- } catch (e) {
- return {
- status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror'
- };
- }
- };
-
- 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)));
-
- quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
- if (!quote) {
- if (results.some(x => x.status === 'temperror')) {
- throw 'quote resolve failed';
- }
- }
- }
-
- const cw = note.summary === '' ? null : note.summary;
-
- // テキストのパース
- const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null);
-
- // vote
- if (reply && reply.hasPoll) {
- const poll = await Polls.findOneOrFail(reply.id);
-
- const tryCreateVote = async (name: string, index: number): Promise<null> => {
- if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
- logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
- } else if (index >= 0) {
- logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
- await vote(actor, reply, index);
-
- // リモートフォロワーにUpdate配信
- deliverQuestionUpdate(reply.id);
- }
- return null;
- };
-
- if (note.name) {
- return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
- }
- }
-
- const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => {
- logger.info(`extractEmojis: ${e}`);
- return [] as Emoji[];
- });
-
- const apEmojis = emojis.map(emoji => emoji.name);
-
- const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
-
- // ユーザーの情報が古かったらついでに更新しておく
- if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
- if (actor.uri) updatePerson(actor.uri);
- }
-
- if (isTalk) {
- for (const recipient of visibleUsers) {
- await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
- return null;
- }
- }
-
- return await post(actor, {
- createdAt: note.published ? new Date(note.published) : null,
- files,
- reply,
- renote: quote,
- name: note.name,
- cw,
- text,
- viaMobile: false,
- localOnly: false,
- visibility,
- visibleUsers,
- apMentions,
- apHashtags,
- apEmojis,
- poll,
- uri: note.id,
- url: getOneApHrefNullable(note.url),
- }, silent);
-}
-
-/**
- * Noteを解決します。
- *
- * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
- const uri = typeof value === 'string' ? value : value.id;
- if (uri == null) throw new Error('missing uri');
-
- // ブロックしてたら中断
- const meta = await fetchMeta();
- if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
-
- const unlock = await getApLock(uri);
-
- try {
- //#region このサーバーに既に登録されていたらそれを返す
- const exist = await fetchNote(uri);
-
- if (exist) {
- return exist;
- }
- //#endregion
-
- if (uri.startsWith(config.url)) {
- throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
- }
-
- // リモートサーバーからフェッチしてきて登録
- // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
- // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
- return await createNote(uri, resolver, true);
- } finally {
- unlock();
- }
-}
-
-export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
- host = toPuny(host);
-
- if (!tags) return [];
-
- const eomjiTags = toArray(tags).filter(isEmoji);
-
- return await Promise.all(eomjiTags.map(async tag => {
- const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
- tag.icon = toSingle(tag.icon);
-
- const exists = await Emojis.findOne({
- host,
- name
- });
-
- if (exists) {
- if ((tag.updated != null && 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.url)
- ) {
- await Emojis.update({
- host,
- name,
- }, {
- uri: tag.id,
- url: tag.icon!.url,
- updatedAt: new Date(),
- });
-
- return await Emojis.findOne({
- host,
- name
- }) as Emoji;
- }
-
- return exists;
- }
-
- logger.info(`register emoji host=${host}, name=${name}`);
-
- return await Emojis.save({
- id: genId(),
- host,
- name,
- uri: tag.id,
- url: tag.icon!.url,
- updatedAt: new Date(),
- aliases: []
- } as Partial<Emoji>);
- }));
-}
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
deleted file mode 100644
index eb8c00a10b..0000000000
--- a/src/remote/activitypub/models/person.ts
+++ /dev/null
@@ -1,494 +0,0 @@
-import { URL } from 'url';
-import * as promiseLimit from 'promise-limit';
-
-import $, { Context } from 'cafy';
-import config from '@/config/index';
-import Resolver from '../resolver';
-import { resolveImage } from './image';
-import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type';
-import { fromHtml } from '../../../mfm/from-html';
-import { htmlToMfm } from '../misc/html-to-mfm';
-import { resolveNote, extractEmojis } from './note';
-import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
-import { extractApHashtags } from './tag';
-import { apLogger } from '../logger';
-import { Note } from '@/models/entities/note';
-import { updateUsertags } from '@/services/update-hashtag';
-import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { Emoji } from '@/models/entities/emoji';
-import { UserNotePining } from '@/models/entities/user-note-pining';
-import { genId } from '@/misc/gen-id';
-import { instanceChart, usersChart } from '@/services/chart/index';
-import { UserPublickey } from '@/models/entities/user-publickey';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-import { toPuny } from '@/misc/convert-host';
-import { UserProfile } from '@/models/entities/user-profile';
-import { getConnection } from 'typeorm';
-import { toArray } from '@/prelude/array';
-import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-import { truncate } from '@/misc/truncate';
-import { StatusError } from '@/misc/fetch';
-
-const logger = apLogger;
-
-const nameLength = 128;
-const summaryLength = 2048;
-
-/**
- * Validate and convert to actor object
- * @param x Fetched object
- * @param uri Fetch target URI
- */
-function validateActor(x: IObject, uri: string): IActor {
- const expectHost = toPuny(new URL(uri).hostname);
-
- if (x == null) {
- throw new Error('invalid Actor: object is null');
- }
-
- if (!isActor(x)) {
- throw new Error(`invalid Actor type '${x.type}'`);
- }
-
- const validate = (name: string, value: any, validater: Context) => {
- const e = validater.test(value);
- if (e) throw new Error(`invalid Actor: ${name} ${e.message}`);
- };
-
- validate('id', x.id, $.str.min(1));
- validate('inbox', x.inbox, $.str.min(1));
- validate('preferredUsername', x.preferredUsername, $.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/));
-
- // These fields are only informational, and some AP software allows these
- // fields to be very long. If they are too long, we cut them off. This way
- // we can at least see these users and their activities.
- validate('name', truncate(x.name, nameLength), $.optional.nullable.str);
- validate('summary', truncate(x.summary, summaryLength), $.optional.nullable.str);
-
- const idHost = toPuny(new URL(x.id!).hostname);
- if (idHost !== expectHost) {
- throw new Error('invalid Actor: id has different host');
- }
-
- if (x.publicKey) {
- if (typeof x.publicKey.id !== 'string') {
- throw new Error('invalid Actor: publicKey.id is not a string');
- }
-
- const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
- if (publicKeyIdHost !== expectHost) {
- throw new Error('invalid Actor: publicKey.id has different host');
- }
- }
-
- return x;
-}
-
-/**
- * Personをフェッチします。
- *
- * Misskeyに対象のPersonが登録されていればそれを返します。
- */
-export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
- // URIがこのサーバーを指しているならデータベースからフェッチ
- if (uri.startsWith(config.url + '/')) {
- const id = uri.split('/').pop();
- return await Users.findOne(id).then(x => x || null);
- }
-
- //#region このサーバーに既に登録されていたらそれを返す
- const exist = await Users.findOne({ uri });
-
- if (exist) {
- return exist;
- }
- //#endregion
-
- return null;
-}
-
-/**
- * Personを作成します。
- */
-export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
- if (uri.startsWith(config.url)) {
- throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
- }
-
- if (resolver == null) resolver = new Resolver();
-
- const object = await resolver.resolve(uri) as any;
-
- const person = validateActor(object, uri);
-
- logger.info(`Creating the Person: ${person.id}`);
-
- const host = toPuny(new URL(object.id).hostname);
-
- const { fields } = analyzeAttachments(person.attachment || []);
-
- const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
-
- const isBot = getApType(object) === 'Service';
-
- const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
-
- // Create user
- let user: IRemoteUser;
- try {
- // Start transaction
- await getConnection().transaction(async transactionalEntityManager => {
- user = await transactionalEntityManager.save(new User({
- id: genId(),
- avatarId: null,
- bannerId: null,
- createdAt: new Date(),
- lastFetchedAt: new Date(),
- name: truncate(person.name, nameLength),
- isLocked: !!person.manuallyApprovesFollowers,
- isExplorable: !!person.discoverable,
- username: person.preferredUsername,
- usernameLower: person.preferredUsername!.toLowerCase(),
- host,
- inbox: person.inbox,
- sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
- followersUri: person.followers ? getApId(person.followers) : undefined,
- featured: person.featured ? getApId(person.featured) : undefined,
- uri: person.id,
- tags,
- isBot,
- isCat: (person as any).isCat === true
- })) as IRemoteUser;
-
- await transactionalEntityManager.save(new UserProfile({
- userId: user.id,
- description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
- url: getOneApHrefNullable(person.url),
- fields,
- birthday: bday ? bday[0] : null,
- location: person['vcard:Address'] || null,
- userHost: host
- }));
-
- if (person.publicKey) {
- await transactionalEntityManager.save(new UserPublickey({
- userId: user.id,
- keyId: person.publicKey.id,
- keyPem: person.publicKey.publicKeyPem
- }));
- }
- });
- } catch (e) {
- // duplicate key error
- if (isDuplicateKeyValueError(e)) {
- // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
- const u = await Users.findOne({
- uri: person.id
- });
-
- if (u) {
- user = u as IRemoteUser;
- } else {
- throw new Error('already registered');
- }
- } else {
- logger.error(e);
- throw e;
- }
- }
-
- // Register host
- registerOrFetchInstanceDoc(host).then(i => {
- Instances.increment({ id: i.id }, 'usersCount', 1);
- instanceChart.newUser(i.host);
- fetchInstanceMetadata(i);
- });
-
- usersChart.update(user!, true);
-
- // ハッシュタグ更新
- updateUsertags(user!, tags);
-
- //#region アバターとヘッダー画像をフェッチ
- const [avatar, banner] = await Promise.all([
- person.icon,
- person.image
- ].map(img =>
- img == null
- ? Promise.resolve(null)
- : resolveImage(user!, img).catch(() => null)
- ));
-
- const avatarId = avatar ? avatar.id : null;
- const bannerId = banner ? banner.id : null;
- const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
- const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
- const avatarBlurhash = avatar ? avatar.blurhash : null;
- const bannerBlurhash = banner ? banner.blurhash : null;
-
- await Users.update(user!.id, {
- avatarId,
- bannerId,
- avatarUrl,
- bannerUrl,
- avatarBlurhash,
- bannerBlurhash
- });
-
- user!.avatarId = avatarId;
- user!.bannerId = bannerId;
- user!.avatarUrl = avatarUrl;
- user!.bannerUrl = bannerUrl;
- user!.avatarBlurhash = avatarBlurhash;
- user!.bannerBlurhash = bannerBlurhash;
- //#endregion
-
- //#region カスタム絵文字取得
- const emojis = await extractEmojis(person.tag || [], host).catch(e => {
- logger.info(`extractEmojis: ${e}`);
- return [] as Emoji[];
- });
-
- const emojiNames = emojis.map(emoji => emoji.name);
-
- await Users.update(user!.id, {
- emojis: emojiNames
- });
- //#endregion
-
- await updateFeatured(user!.id).catch(err => logger.error(err));
-
- return user!;
-}
-
-/**
- * Personの情報を更新します。
- * Misskeyに対象のPersonが登録されていなければ無視します。
- * @param uri URI of Person
- * @param resolver Resolver
- * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
- */
-export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
- // URIがこのサーバーを指しているならスキップ
- if (uri.startsWith(config.url + '/')) {
- return;
- }
-
- //#region このサーバーに既に登録されているか
- const exist = await Users.findOne({ uri }) as IRemoteUser;
-
- if (exist == null) {
- return;
- }
- //#endregion
-
- if (resolver == null) resolver = new Resolver();
-
- const object = hint || await resolver.resolve(uri) as any;
-
- const person = validateActor(object, uri);
-
- logger.info(`Updating the Person: ${person.id}`);
-
- // アバターとヘッダー画像をフェッチ
- const [avatar, banner] = await Promise.all([
- person.icon,
- person.image
- ].map(img =>
- img == null
- ? Promise.resolve(null)
- : resolveImage(exist, img).catch(() => null)
- ));
-
- // カスタム絵文字取得
- const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
- logger.info(`extractEmojis: ${e}`);
- return [] as Emoji[];
- });
-
- const emojiNames = emojis.map(emoji => emoji.name);
-
- const { fields } = analyzeAttachments(person.attachment || []);
-
- const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
-
- const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
-
- const updates = {
- lastFetchedAt: new Date(),
- inbox: person.inbox,
- sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
- followersUri: person.followers ? getApId(person.followers) : undefined,
- featured: person.featured,
- emojis: emojiNames,
- name: truncate(person.name, nameLength),
- tags,
- isBot: getApType(object) === 'Service',
- isCat: (person as any).isCat === true,
- isLocked: !!person.manuallyApprovesFollowers,
- isExplorable: !!person.discoverable,
- } as Partial<User>;
-
- if (avatar) {
- updates.avatarId = avatar.id;
- updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
- updates.avatarBlurhash = avatar.blurhash;
- }
-
- if (banner) {
- updates.bannerId = banner.id;
- updates.bannerUrl = DriveFiles.getPublicUrl(banner);
- updates.bannerBlurhash = banner.blurhash;
- }
-
- // Update user
- await Users.update(exist.id, updates);
-
- if (person.publicKey) {
- await UserPublickeys.update({ userId: exist.id }, {
- keyId: person.publicKey.id,
- keyPem: person.publicKey.publicKeyPem
- });
- }
-
- await UserProfiles.update({ userId: exist.id }, {
- url: getOneApHrefNullable(person.url),
- fields,
- description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
- birthday: bday ? bday[0] : null,
- location: person['vcard:Address'] || null,
- });
-
- // ハッシュタグ更新
- updateUsertags(exist, tags);
-
- // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
- await Followings.update({
- followerId: exist.id
- }, {
- followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
- });
-
- await updateFeatured(exist.id).catch(err => logger.error(err));
-}
-
-/**
- * Personを解決します。
- *
- * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
- if (typeof uri !== 'string') throw new Error('uri is not string');
-
- //#region このサーバーに既に登録されていたらそれを返す
- const exist = await fetchPerson(uri);
-
- if (exist) {
- return exist;
- }
- //#endregion
-
- // リモートサーバーからフェッチしてきて登録
- if (resolver == null) resolver = new Resolver();
- return await createPerson(uri, resolver);
-}
-
-const services: {
- [x: string]: (id: string, username: string) => any
- } = {
- 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
- 'misskey:authentication:github': (id, login) => ({ id, login }),
- 'misskey:authentication:discord': (id, name) => $discord(id, name)
-};
-
-const $discord = (id: string, name: string) => {
- if (typeof name !== 'string')
- name = 'unknown#0000';
- const [username, discriminator] = name.split('#');
- return { id, username, discriminator };
-};
-
-function addService(target: { [x: string]: any }, source: IApPropertyValue) {
- const service = services[source.name];
-
- if (typeof source.value !== 'string')
- source.value = 'unknown';
-
- const [id, username] = source.value.split('@');
-
- if (service)
- target[source.name.split(':')[2]] = service(id, username);
-}
-
-export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
- const fields: {
- name: string,
- value: string
- }[] = [];
- const services: { [x: string]: any } = {};
-
- if (Array.isArray(attachments)) {
- for (const attachment of attachments.filter(isPropertyValue)) {
- if (isPropertyValue(attachment.identifier)) {
- addService(services, attachment.identifier);
- } else {
- fields.push({
- name: attachment.name,
- value: fromHtml(attachment.value)
- });
- }
- }
- }
-
- return { fields, services };
-}
-
-export async function updateFeatured(userId: User['id']) {
- const user = await Users.findOneOrFail(userId);
- if (!Users.isRemoteUser(user)) return;
- if (!user.featured) return;
-
- logger.info(`Updating the featured: ${user.uri}`);
-
- const resolver = new Resolver();
-
- // Resolve to (Ordered)Collection Object
- const collection = await resolver.resolveCollection(user.featured);
- if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
-
- // Resolve to Object(may be Note) arrays
- const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
- const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
-
- // Resolve and regist Notes
- const limit = promiseLimit<Note | null>(2);
- const featuredNotes = await Promise.all(items
- .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
- .slice(0, 5)
- .map(item => limit(() => resolveNote(item, resolver))));
-
- await getConnection().transaction(async transactionalEntityManager => {
- await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
-
- // とりあえずidを別の時間で生成して順番を維持
- let td = 0;
- for (const note of featuredNotes.filter(note => note != null)) {
- td -= 1000;
- transactionalEntityManager.insert(UserNotePining, {
- id: genId(new Date(Date.now() + td)),
- createdAt: new Date(),
- userId: user.id,
- noteId: note!.id
- });
- }
- });
-}
diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts
deleted file mode 100644
index 79f93c3a30..0000000000
--- a/src/remote/activitypub/models/question.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import config from '@/config/index';
-import Resolver from '../resolver';
-import { IObject, IQuestion, isQuestion, } from '../type';
-import { apLogger } from '../logger';
-import { Notes, Polls } from '@/models/index';
-import { IPoll } from '@/models/entities/poll';
-
-export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
- if (resolver == null) resolver = new Resolver();
-
- const question = await resolver.resolve(source);
-
- if (!isQuestion(question)) {
- throw new Error('invalid type');
- }
-
- 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 votes = question[multiple ? 'anyOf' : 'oneOf']!
- .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
-
- return {
- choices,
- votes,
- multiple,
- expiresAt
- };
-}
-
-/**
- * Update votes of Question
- * @param uri URI of AP Question object
- * @returns true if updated
- */
-export async function updateQuestion(value: any) {
- const uri = typeof value === 'string' ? value : value.id;
-
- // URIがこのサーバーを指しているならスキップ
- if (uri.startsWith(config.url + '/')) throw new Error('uri points local');
-
- //#region このサーバーに既に登録されているか
- const note = await Notes.findOne({ uri });
- if (note == null) throw new Error('Question is not registed');
-
- const poll = await Polls.findOne({ noteId: note.id });
- if (poll == null) throw new Error('Question is not registed');
- //#endregion
-
- // resolve new Question object
- const resolver = new Resolver();
- const question = await resolver.resolve(value) as IQuestion;
- apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
-
- if (question.type !== 'Question') throw new Error('object is not a Question');
-
- const apChoices = question.oneOf || question.anyOf;
-
- 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;
-
- if (oldCount != newCount) {
- changed = true;
- poll.votes[poll.choices.indexOf(choice)] = newCount;
- }
- }
-
- await Polls.update({ noteId: note.id }, {
- votes: poll.votes
- });
-
- return changed;
-}
diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts
deleted file mode 100644
index fbc6b9b428..0000000000
--- a/src/remote/activitypub/models/tag.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { toArray } from '@/prelude/array';
-import { IObject, isHashtag, IApHashtag } from '../type';
-
-export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
- if (tags == null) return [];
-
- const hashtags = extractApHashtagObjects(tags);
-
- return hashtags.map(tag => {
- const m = tag.name.match(/^#(.+)/);
- return m ? m[1] : null;
- }).filter((x): x is string => x != null);
-}
-
-export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
- if (tags == null) return [];
- return toArray(tags).filter(isHashtag);
-}