diff options
Diffstat (limited to 'src/remote/activitypub/models/person.ts')
| -rw-r--r-- | src/remote/activitypub/models/person.ts | 242 |
1 files changed, 104 insertions, 138 deletions
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index d27c937988..51a9efa10b 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,29 +1,29 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; import { toUnicode } from 'punycode'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; -import { IDriveFile } from '../../../models/drive-file'; -import Meta from '../../../models/meta'; +import { DriveFile } from '../../../models/entities/drive-file'; import { fromHtml } from '../../../mfm/fromHtml'; -import usersChart from '../../../services/chart/users'; -import instanceChart from '../../../services/chart/instance'; import { URL } from 'url'; import { resolveNote, extractEmojis } from './note'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; -import Instance from '../../../models/instance'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; -import { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import Following from '../../../models/following'; import { IIdentifier } from './identifier'; import { apLogger } from '../logger'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { updateHashtag } from '../../../services/update-hashtag'; +import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models'; +import { User, IRemoteUser } from '../../../models/entities/user'; +import { Emoji } from '../../../models/entities/emoji'; +import { UserNotePining } from '../../../models/entities/user-note-pinings'; +import { genId } from '../../../misc/gen-id'; +import { UserServiceLinking } from '../../../models/entities/user-service-linking'; +import { instanceChart, usersChart } from '../../../services/chart'; +import { UserPublickey } from '../../../models/entities/user-publickey'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; const logger = apLogger; /** @@ -50,11 +50,11 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: inbox is not a string'); } - if (!validateUsername(x.preferredUsername, true)) { + if (!Users.validateUsername(x.preferredUsername, true)) { return new Error('invalid person: invalid username'); } - if (!isValidName(x.name == '' ? null : x.name)) { + if (!Users.isValidName(x.name == '' ? null : x.name)) { return new Error('invalid person: invalid name'); } @@ -84,17 +84,17 @@ function validatePerson(x: any, uri: string) { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { +export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await User.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Users.findOne(id); } //#region このサーバーに既に登録されていたらそれを返す - const exist = await User.findOne({ uri }); + const exist = await Users.findOne({ uri }); if (exist) { return exist; @@ -107,7 +107,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs /** * Personを作成します。 */ -export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { +export async function createPerson(uri: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; if (resolver == null) resolver = new Resolver(); @@ -124,21 +124,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU logger.info(`Creating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - const host = toUnicode(new URL(object.id).hostname.toLowerCase()); const { fields, services } = analyzeAttachments(person.attachment); @@ -150,24 +135,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Create user let user: IRemoteUser; try { - user = await User.insert({ + user = await Users.save({ + id: genId(), avatarId: null, bannerId: null, - createdAt: Date.parse(person.published) || null, + createdAt: Date.parse(person.published) || new Date(), lastFetchedAt: new Date(), description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, name: person.name, isLocked: person.manuallyApprovesFollowers, username: person.preferredUsername, usernameLower: person.preferredUsername.toLowerCase(), host, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, inbox: person.inbox, sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), featured: person.featured, @@ -179,10 +158,22 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU tags, isBot, isCat: (person as any).isCat === true - }) as IRemoteUser; + } as Partial<User>) as IRemoteUser; + + await UserPublickeys.save({ + id: genId(), + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + } as UserPublickey); + + await UserServiceLinkings.save({ + id: genId(), + userId: user.id, + } as UserServiceLinking); } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new Error('already registered'); } @@ -190,33 +181,25 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU throw e; } + await UserServiceLinkings.save({ + id: genId(), + userId: user.id + } as UserServiceLinking); + // Register host registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - usersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'usersCount', 1); instanceChart.newUser(i.host); }); - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1 - } - }, { upsert: true }); - usersChart.update(user, true); - //#endregion // ハッシュタグ更新 for (const tag of tags) updateHashtag(user, tag, true, true); for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); //#region アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile>([ person.icon, person.image ].map(img => @@ -225,22 +208,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU : resolveImage(user, img).catch(() => null) ))); - const avatarId = avatar ? avatar._id : null; - const bannerId = banner ? banner._id : null; - const avatarUrl = getDriveFileUrl(avatar, true); - const bannerUrl = getDriveFileUrl(banner, false); - const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; - const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + const avatarUrl = DriveFiles.getPublicUrl(avatar); + const bannerUrl = DriveFiles.getPublicUrl(banner); + const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; + const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null; - await User.update({ _id: user._id }, { - $set: { - avatarId, - bannerId, - avatarUrl, - bannerUrl, - avatarColor, - bannerColor - } + await Users.update(user.id, { + avatarId, + bannerId, + avatarUrl, + bannerUrl, + avatarColor, + bannerColor }); user.avatarId = avatarId; @@ -254,19 +235,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU //#region カスタム絵文字取得 const emojis = await extractEmojis(person.tag, host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - await User.update({ _id: user._id }, { - $set: { - emojis: emojiNames - } + await Users.update(user.id, { + emojis: emojiNames }); //#endregion - await updateFeatured(user._id).catch(err => logger.error(err)); + await updateFeatured(user.id).catch(err => logger.error(err)); return user; } @@ -287,7 +266,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje } //#region このサーバーに既に登録されているか - const exist = await User.findOne({ uri }) as IRemoteUser; + const exist = await Users.findOne({ uri }) as IRemoteUser; if (exist == null) { return; @@ -295,10 +274,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje //#endregion // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await User.update({ _id: exist._id }, { - $set: { - lastFetchedAt: new Date(), - }, + await Users.update(exist.id, { + lastFetchedAt: new Date(), }); if (resolver == null) resolver = new Resolver(); @@ -315,23 +292,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje logger.info(`Updating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - // アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile>([ person.icon, person.image ].map(img => @@ -343,7 +305,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje // カスタム絵文字取得 const emojis = await extractEmojis(person.tag, exist.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); @@ -359,40 +321,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje featured: person.featured, emojis: emojiNames, description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, name: person.name, url: person.url, endpoints: person.endpoints, fields, - ...services, tags, isBot: object.type == 'Service', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, - createdAt: Date.parse(person.published) || null, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - } as any; + createdAt: new Date(Date.parse(person.published)) || null, + } as Partial<User>; if (avatar) { - updates.avatarId = avatar._id; - updates.avatarUrl = getDriveFileUrl(avatar, true); - updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; + updates.avatarId = avatar.id; + updates.avatarUrl = DriveFiles.getPublicUrl(avatar); + updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null; } if (banner) { - updates.bannerId = banner._id; - updates.bannerUrl = getDriveFileUrl(banner, true); - updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + updates.bannerId = banner.id; + updates.bannerUrl = DriveFiles.getPublicUrl(banner); + updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null; } // Update user - await User.update({ _id: exist._id }, { - $set: updates + await Users.update(exist.id, updates); + + await UserPublickeys.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + }); + + await UserServiceLinkings.update({ userId: exist.id }, { + twitterUserId: services.twitter.userId, + twitterScreenName: services.twitter.screenName, + githubId: services.github.id, + githubLogin: services.github.login, + discordId: services.discord.id, + discordUsername: services.discord.username, + discordDiscriminator: services.discord.discriminator, }); // ハッシュタグ更新 @@ -400,17 +367,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false); // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await Following.update({ - followerId: exist._id + await Followings.update({ + followerId: exist.id }, { - $set: { - '_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) - } - }, { - multi: true + followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) }); - await updateFeatured(exist._id).catch(err => logger.error(err)); + await updateFeatured(exist.id).catch(err => logger.error(err)); } /** @@ -419,7 +382,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> { +export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> { if (typeof uri !== 'string') throw 'uri is not string'; //#region このサーバーに既に登録されていたらそれを返す @@ -492,9 +455,9 @@ export function analyzeAttachments(attachments: ITag[]) { return { fields, services }; } -export async function updateFeatured(userId: mongo.ObjectID) { - const user = await User.findOne({ _id: userId }); - if (!isRemoteUser(user)) return; +export async function updateFeatured(userId: User['id']) { + const user = await Users.findOne(userId); + if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); @@ -515,11 +478,14 @@ export async function updateFeatured(userId: mongo.ObjectID) { const featuredNotes = await Promise.all(items .filter(item => item.type === 'Note') .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>)); + .map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>)); - await User.update({ _id: user._id }, { - $set: { - pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id) - } - }); + for (const note of featuredNotes.filter(note => note != null)) { + UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id + } as UserNotePining); + } } |