diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
| commit | 0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch) | |
| tree | 40874799472fa07416f17b50a398ac33b7771905 /src/remote | |
| parent | update deps (diff) | |
| download | sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2 sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip | |
refactoring
Resolve #7779
Diffstat (limited to 'src/remote')
79 files changed, 0 insertions, 4248 deletions
diff --git a/src/remote/activitypub/ap-request.ts b/src/remote/activitypub/ap-request.ts deleted file mode 100644 index 76a3857140..0000000000 --- a/src/remote/activitypub/ap-request.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as crypto from 'crypto'; -import { URL } from 'url'; - -type Request = { - url: string; - method: string; - headers: Record<string, string>; -}; - -type PrivateKey = { - privateKeyPem: string; - keyId: string; -}; - -export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }) { - const u = new URL(args.url); - const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; - - const request: Request = { - url: u.href, - method: 'POST', - headers: objectAssignWithLcKey({ - 'Date': new Date().toUTCString(), - 'Host': u.hostname, - 'Content-Type': 'application/activity+json', - 'Digest': digestHeader, - }, args.additionalHeaders), - }; - - const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; -} - -export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }) { - const u = new URL(args.url); - - const request: Request = { - url: u.href, - method: 'GET', - headers: objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json', - 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).hostname, - }, args.additionalHeaders), - }; - - const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; -} - -function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) { - const signingString = genSigningString(request, includeHeaders); - const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); - const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; - - request.headers = objectAssignWithLcKey(request.headers, { - Signature: signatureHeader - }); - - return { - request, - signingString, - signature, - signatureHeader, - }; -} - -function genSigningString(request: Request, includeHeaders: string[]) { - request.headers = lcObjectKey(request.headers); - - const results: string[] = []; - - for (const key of includeHeaders.map(x => x.toLowerCase())) { - if (key === '(request-target)') { - results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); - } else { - results.push(`${key}: ${request.headers[key]}`); - } - } - - return results.join('\n'); -} - -function lcObjectKey(src: Record<string, string>) { - const dst: Record<string, string> = {}; - for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; - return dst; -} - -function objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>) { - return Object.assign(lcObjectKey(a), lcObjectKey(b)); -} diff --git a/src/remote/activitypub/audience.ts b/src/remote/activitypub/audience.ts deleted file mode 100644 index 3d2dab1459..0000000000 --- a/src/remote/activitypub/audience.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ApObject, getApIds } from './type'; -import Resolver from './resolver'; -import { resolvePerson } from './models/person'; -import { unique, concat } from '@/prelude/array'; -import * as promiseLimit from 'promise-limit'; -import { User, IRemoteUser } from '@/models/entities/user'; - -type Visibility = 'public' | 'home' | 'followers' | 'specified'; - -type AudienceInfo = { - visibility: Visibility, - mentionedUsers: User[], - visibleUsers: User[], -}; - -export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> { - const toGroups = groupingAudience(getApIds(to), actor); - const ccGroups = groupingAudience(getApIds(cc), actor); - - const others = unique(concat([toGroups.other, ccGroups.other])); - - const limit = promiseLimit<User | null>(2); - const mentionedUsers = (await Promise.all( - others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))) - )).filter((x): x is User => x != null); - - if (toGroups.public.length > 0) { - return { - visibility: 'public', - mentionedUsers, - visibleUsers: [] - }; - } - - if (ccGroups.public.length > 0) { - return { - visibility: 'home', - mentionedUsers, - visibleUsers: [] - }; - } - - if (toGroups.followers.length > 0) { - return { - visibility: 'followers', - mentionedUsers, - visibleUsers: [] - }; - } - - return { - visibility: 'specified', - mentionedUsers, - visibleUsers: mentionedUsers - }; -} - -function groupingAudience(ids: string[], actor: IRemoteUser) { - const groups = { - public: [] as string[], - followers: [] as string[], - other: [] as string[], - }; - - for (const id of ids) { - if (isPublic(id)) { - groups.public.push(id); - } else if (isFollowers(id, actor)) { - groups.followers.push(id); - } else { - groups.other.push(id); - } - } - - groups.other = unique(groups.other); - - return groups; -} - -function isPublic(id: string) { - return [ - 'https://www.w3.org/ns/activitystreams#Public', - 'as#Public', - 'Public', - ].includes(id); -} - -function isFollowers(id: string, actor: IRemoteUser) { - return ( - id === (actor.followersUri || `${actor.uri}/followers`) - ); -} diff --git a/src/remote/activitypub/db-resolver.ts b/src/remote/activitypub/db-resolver.ts deleted file mode 100644 index 289b6f0ee8..0000000000 --- a/src/remote/activitypub/db-resolver.ts +++ /dev/null @@ -1,140 +0,0 @@ -import config from '@/config/index'; -import { Note } from '@/models/entities/note'; -import { User, IRemoteUser } from '@/models/entities/user'; -import { UserPublickey } from '@/models/entities/user-publickey'; -import { MessagingMessage } from '@/models/entities/messaging-message'; -import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index'; -import { IObject, getApId } from './type'; -import { resolvePerson } from './models/person'; -import escapeRegexp = require('escape-regexp'); - -export default class DbResolver { - constructor() { - } - - /** - * AP Note => Misskey Note in DB - */ - public async getNoteFromApId(value: string | IObject): Promise<Note | null> { - const parsed = this.parseUri(value); - - if (parsed.id) { - return (await Notes.findOne({ - id: parsed.id - })) || null; - } - - if (parsed.uri) { - return (await Notes.findOne({ - uri: parsed.uri - })) || null; - } - - return null; - } - - public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> { - const parsed = this.parseUri(value); - - if (parsed.id) { - return (await MessagingMessages.findOne({ - id: parsed.id - })) || null; - } - - if (parsed.uri) { - return (await MessagingMessages.findOne({ - uri: parsed.uri - })) || null; - } - - return null; - } - - /** - * AP Person => Misskey User in DB - */ - public async getUserFromApId(value: string | IObject): Promise<User | null> { - const parsed = this.parseUri(value); - - if (parsed.id) { - return (await Users.findOne({ - id: parsed.id - })) || null; - } - - if (parsed.uri) { - return (await Users.findOne({ - uri: parsed.uri - })) || null; - } - - return null; - } - - /** - * AP KeyId => Misskey User and Key - */ - public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> { - const key = await UserPublickeys.findOne({ - keyId - }); - - if (key == null) return null; - - const user = await Users.findOne(key.userId) as IRemoteUser; - - return { - user, - key - }; - } - - /** - * AP Actor id => Misskey User and Key - */ - public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> { - const user = await resolvePerson(uri) as IRemoteUser; - - if (user == null) return null; - - const key = await UserPublickeys.findOne(user.id); - - return { - user, - key - }; - } - - public parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - type: matchLocal[1], - id: matchLocal[2] - }; - } else { - return { - uri - }; - } - } -} - -export type AuthUser = { - user: IRemoteUser; - key?: UserPublickey; -}; - -type UriParseResult = { - /** id in DB (local object only) */ - id?: string; - /** uri in DB (remote object only) */ - uri?: string; - /** hint of type (local object only, ex: notes, users) */ - type?: string -}; diff --git a/src/remote/activitypub/deliver-manager.ts b/src/remote/activitypub/deliver-manager.ts deleted file mode 100644 index d37f97a447..0000000000 --- a/src/remote/activitypub/deliver-manager.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Users, Followings } from '@/models/index'; -import { ILocalUser, IRemoteUser, User } from '@/models/entities/user'; -import { deliver } from '@/queue/index'; - -//#region types -interface IRecipe { - type: string; -} - -interface IFollowersRecipe extends IRecipe { - type: 'Followers'; -} - -interface IDirectRecipe extends IRecipe { - type: 'Direct'; - to: IRemoteUser; -} - -const isFollowers = (recipe: any): recipe is IFollowersRecipe => - recipe.type === 'Followers'; - -const isDirect = (recipe: any): recipe is IDirectRecipe => - recipe.type === 'Direct'; -//#endregion - -export default class DeliverManager { - private actor: { id: User['id']; host: null; }; - private activity: any; - private recipes: IRecipe[] = []; - - /** - * Constructor - * @param actor Actor - * @param activity Activity to deliver - */ - constructor(actor: { id: User['id']; host: null; }, activity: any) { - this.actor = actor; - this.activity = activity; - } - - /** - * Add recipe for followers deliver - */ - public addFollowersRecipe() { - const deliver = { - type: 'Followers' - } as IFollowersRecipe; - - this.addRecipe(deliver); - } - - /** - * Add recipe for direct deliver - * @param to To - */ - public addDirectRecipe(to: IRemoteUser) { - const recipe = { - type: 'Direct', - to - } as IDirectRecipe; - - this.addRecipe(recipe); - } - - /** - * Add recipe - * @param recipe Recipe - */ - public addRecipe(recipe: IRecipe) { - this.recipes.push(recipe); - } - - /** - * Execute delivers - */ - public async execute() { - if (!Users.isLocalUser(this.actor)) return; - - const inboxes = new Set<string>(); - - // build inbox list - for (const recipe of this.recipes) { - if (isFollowers(recipe)) { - // followers deliver - const followers = await Followings.find({ - followeeId: this.actor.id - }); - - for (const following of followers) { - if (Followings.isRemoteFollower(following)) { - const inbox = following.followerSharedInbox || following.followerInbox; - inboxes.add(inbox); - } - } - } else if (isDirect(recipe)) { - // direct deliver - const inbox = recipe.to.inbox; - if (inbox) inboxes.add(inbox); - } - } - - // deliver - for (const inbox of inboxes) { - deliver(this.actor, this.activity, inbox); - } - } -} - -//#region Utilities -/** - * Deliver activity to followers - * @param activity Activity - * @param from Followee - */ -export async function deliverToFollowers(actor: ILocalUser, activity: any) { - const manager = new DeliverManager(actor, activity); - manager.addFollowersRecipe(); - await manager.execute(); -} - -/** - * Deliver activity to user - * @param activity Activity - * @param to Target user - */ -export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) { - const manager = new DeliverManager(actor, activity); - manager.addDirectRecipe(to); - await manager.execute(); -} -//#endregion diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts deleted file mode 100644 index 1afb733ab5..0000000000 --- a/src/remote/activitypub/kernel/accept/follow.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import accept from '@/services/following/requests/accept'; -import { IFollow } from '../../type'; -import DbResolver from '../../db-resolver'; -import { relayAccepted } from '@/services/relay'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - - const dbResolver = new DbResolver(); - const follower = await dbResolver.getUserFromApId(activity.actor); - - if (follower == null) { - return `skip: follower not found`; - } - - if (follower.host != null) { - return `skip: follower is not a local user`; - } - - // relay - const match = activity.id?.match(/follow-relay\/(\w+)/); - if (match) { - return await relayAccepted(match[1]); - } - - await accept(actor, follower); - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts deleted file mode 100644 index 5c6f81b2e3..0000000000 --- a/src/remote/activitypub/kernel/accept/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '@/models/entities/user'; -import acceptFollow from './follow'; -import { IAccept, isFollow, getApType } from '../../type'; -import { apLogger } from '../../logger'; - -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => { - const uri = activity.id || activity; - - logger.info(`Accept: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await acceptFollow(actor, object); - - return `skip: Unknown Accept type: ${getApType(object)}`; -}; diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts deleted file mode 100644 index b33be0cc85..0000000000 --- a/src/remote/activitypub/kernel/add/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { IAdd } from '../../type'; -import { resolveNote } from '../../models/note'; -import { addPinned } from '@/services/i/pin'; - -export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - if (activity.target == null) { - throw new Error('target is null'); - } - - if (activity.target === actor.featured) { - const note = await resolveNote(activity.object); - if (note == null) throw new Error('note not found'); - await addPinned(actor, note.id); - return; - } - - throw new Error(`unknown target: ${activity.target}`); -}; diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts deleted file mode 100644 index 581357e577..0000000000 --- a/src/remote/activitypub/kernel/announce/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '@/models/entities/user'; -import announceNote from './note'; -import { IAnnounce, getApId } from '../../type'; -import { apLogger } from '../../logger'; - -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => { - const uri = getApId(activity); - - logger.info(`Announce: ${uri}`); - - const resolver = new Resolver(); - - const targetUri = getApId(activity.object); - - announceNote(resolver, actor, activity, targetUri); -}; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts deleted file mode 100644 index 5230867f24..0000000000 --- a/src/remote/activitypub/kernel/announce/note.ts +++ /dev/null @@ -1,67 +0,0 @@ -import Resolver from '../../resolver'; -import post from '@/services/note/create'; -import { IRemoteUser } from '@/models/entities/user'; -import { IAnnounce, getApId } from '../../type'; -import { fetchNote, resolveNote } from '../../models/note'; -import { apLogger } from '../../logger'; -import { extractDbHost } from '@/misc/convert-host'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { getApLock } from '@/misc/app-lock'; -import { parseAudience } from '../../audience'; -import { StatusError } from '@/misc/fetch'; - -const logger = apLogger; - -/** - * アナウンスアクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> { - const uri = getApId(activity); - - // アナウンサーが凍結されていたらスキップ - if (actor.isSuspended) { - return; - } - - // アナウンス先をブロックしてたら中断 - const meta = await fetchMeta(); - if (meta.blockedHosts.includes(extractDbHost(uri))) return; - - const unlock = await getApLock(uri); - - try { - // 既に同じURIを持つものが登録されていないかチェック - const exist = await fetchNote(uri); - if (exist) { - return; - } - - // Announce対象をresolve - let renote; - try { - renote = await resolveNote(targetUri); - } catch (e) { - // 対象が4xxならスキップ - if (e instanceof StatusError && e.isClientError) { - logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`); - return; - } - logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`); - throw e; - } - - logger.info(`Creating the (Re)Note: ${uri}`); - - const activityAudience = await parseAudience(actor, activity.to, activity.cc); - - await post(actor, { - createdAt: activity.published ? new Date(activity.published) : null, - renote, - visibility: activityAudience.visibility, - visibleUsers: activityAudience.visibleUsers, - uri - }); - } finally { - unlock(); - } -} diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts deleted file mode 100644 index 4fd1e07b9b..0000000000 --- a/src/remote/activitypub/kernel/block/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IBlock } from '../../type'; -import block from '@/services/blocking/create'; -import { IRemoteUser } from '@/models/entities/user'; -import DbResolver from '../../db-resolver'; - -export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { - // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず - - const dbResolver = new DbResolver(); - const blockee = await dbResolver.getUserFromApId(activity.object); - - if (blockee == null) { - return `skip: blockee not found`; - } - - if (blockee.host != null) { - return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; - } - - await block(actor, blockee); - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts deleted file mode 100644 index ce039a363b..0000000000 --- a/src/remote/activitypub/kernel/create/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '@/models/entities/user'; -import createNote from './note'; -import { ICreate, getApId, isPost, getApType } from '../../type'; -import { apLogger } from '../../logger'; -import { toArray, concat, unique } from '@/prelude/array'; - -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => { - const uri = getApId(activity); - - logger.info(`Create: ${uri}`); - - // copy audiences between activity <=> object. - if (typeof activity.object === 'object') { - const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); - const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); - - activity.to = to; - activity.cc = cc; - activity.object.to = to; - activity.object.cc = cc; - } - - // If there is no attributedTo, use Activity actor. - if (typeof activity.object === 'object' && !activity.object.attributedTo) { - activity.object.attributedTo = activity.actor; - } - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isPost(object)) { - createNote(resolver, actor, object, false, activity); - } else { - logger.warn(`Unknown type: ${getApType(object)}`); - } -}; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts deleted file mode 100644 index 14e311e4cd..0000000000 --- a/src/remote/activitypub/kernel/create/note.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '@/models/entities/user'; -import { createNote, fetchNote } from '../../models/note'; -import { getApId, IObject, ICreate } from '../../type'; -import { getApLock } from '@/misc/app-lock'; -import { extractDbHost } from '@/misc/convert-host'; -import { StatusError } from '@/misc/fetch'; - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> { - const uri = getApId(note); - - if (typeof note === 'object') { - if (actor.uri !== note.attributedTo) { - return `skip: actor.uri !== note.attributedTo`; - } - - if (typeof note.id === 'string') { - if (extractDbHost(actor.uri) !== extractDbHost(note.id)) { - return `skip: host in actor.uri !== note.id`; - } - } - } - - const unlock = await getApLock(uri); - - try { - const exist = await fetchNote(note); - if (exist) return 'skip: note exists'; - - await createNote(note, resolver, silent); - return 'ok'; - } catch (e) { - if (e instanceof StatusError && e.isClientError) { - return `skip ${e.statusCode}`; - } else { - throw e; - } - } finally { - unlock(); - } -} diff --git a/src/remote/activitypub/kernel/delete/actor.ts b/src/remote/activitypub/kernel/delete/actor.ts deleted file mode 100644 index 502f8d5ab5..0000000000 --- a/src/remote/activitypub/kernel/delete/actor.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { apLogger } from '../../logger'; -import { createDeleteAccountJob } from '@/queue'; -import { IRemoteUser } from '@/models/entities/user'; -import { Users } from '@/models/index'; - -const logger = apLogger; - -export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> { - logger.info(`Deleting the Actor: ${uri}`); - - if (actor.uri !== uri) { - return `skip: delete actor ${actor.uri} !== ${uri}`; - } - - if (actor.isDeleted) { - logger.info(`skip: already deleted`); - } - - const job = await createDeleteAccountJob(actor); - - await Users.update(actor.id, { - isDeleted: true, - }); - - return `ok: queued ${job.name} ${job.id}`; -} diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts deleted file mode 100644 index 86a452de76..0000000000 --- a/src/remote/activitypub/kernel/delete/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import deleteNote from './note'; -import { IRemoteUser } from '@/models/entities/user'; -import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type'; -import { toSingle } from '@/prelude/array'; -import { deleteActor } from './actor'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - // 削除対象objectのtype - let formarType: string | undefined; - - if (typeof activity.object === 'string') { - // typeが不明だけど、どうせ消えてるのでremote resolveしない - formarType = undefined; - } else { - const object = activity.object as IObject; - if (isTombstone(object)) { - formarType = toSingle(object.formerType); - } else { - formarType = toSingle(object.type); - } - } - - const uri = getApId(activity.object); - - // type不明でもactorとobjectが同じならばそれはPersonに違いない - if (!formarType && actor.uri === uri) { - formarType = 'Person'; - } - - // それでもなかったらおそらくNote - if (!formarType) { - formarType = 'Note'; - } - - if (validPost.includes(formarType)) { - return await deleteNote(actor, uri); - } else if (validActor.includes(formarType)) { - return await deleteActor(actor, uri); - } else { - return `Unknown type ${formarType}`; - } -}; diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts deleted file mode 100644 index 3875a33d13..0000000000 --- a/src/remote/activitypub/kernel/delete/note.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import deleteNode from '@/services/note/delete'; -import { apLogger } from '../../logger'; -import DbResolver from '../../db-resolver'; -import { getApLock } from '@/misc/app-lock'; -import { deleteMessage } from '@/services/messages/delete'; - -const logger = apLogger; - -export default async function(actor: IRemoteUser, uri: string): Promise<string> { - logger.info(`Deleting the Note: ${uri}`); - - const unlock = await getApLock(uri); - - try { - const dbResolver = new DbResolver(); - const note = await dbResolver.getNoteFromApId(uri); - - if (note == null) { - const message = await dbResolver.getMessageFromApId(uri); - if (message == null) return 'message not found'; - - if (message.userId !== actor.id) { - return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; - } - - await deleteMessage(message); - - return 'ok: message deleted'; - } - - if (note.userId !== actor.id) { - return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; - } - - await deleteNode(actor, note); - return 'ok: note deleted'; - } finally { - unlock(); - } -} diff --git a/src/remote/activitypub/kernel/flag/index.ts b/src/remote/activitypub/kernel/flag/index.ts deleted file mode 100644 index 7abfd694cd..0000000000 --- a/src/remote/activitypub/kernel/flag/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import config from '@/config/index'; -import { IFlag, getApIds } from '../../type'; -import { AbuseUserReports, Users } from '@/models/index'; -import { In } from 'typeorm'; -import { genId } from '@/misc/gen-id'; - -export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => { - // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので - // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する - const uris = getApIds(activity.object); - - const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()); - const users = await Users.find({ - id: In(userIds) - }); - if (users.length < 1) return `skip`; - - await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: users[0].id, - targetUserHost: users[0].host, - reporterId: actor.id, - reporterHost: actor.host, - comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}` - }); - - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts deleted file mode 100644 index 3183207afa..0000000000 --- a/src/remote/activitypub/kernel/follow.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import follow from '@/services/following/create'; -import { IFollow } from '../type'; -import DbResolver from '../db-resolver'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { - const dbResolver = new DbResolver(); - const followee = await dbResolver.getUserFromApId(activity.object); - - if (followee == null) { - return `skip: followee not found`; - } - - if (followee.host != null) { - return `skip: フォローしようとしているユーザーはローカルユーザーではありません`; - } - - await follow(actor, followee, activity.id); - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts deleted file mode 100644 index 20df28eec6..0000000000 --- a/src/remote/activitypub/kernel/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type'; -import { IRemoteUser } from '@/models/entities/user'; -import create from './create/index'; -import performDeleteActivity from './delete/index'; -import performUpdateActivity from './update/index'; -import { performReadActivity } from './read'; -import follow from './follow'; -import undo from './undo/index'; -import like from './like'; -import announce from './announce/index'; -import accept from './accept/index'; -import reject from './reject/index'; -import add from './add/index'; -import remove from './remove/index'; -import block from './block/index'; -import flag from './flag/index'; -import { apLogger } from '../logger'; -import Resolver from '../resolver'; -import { toArray } from '@/prelude/array'; - -export async function performActivity(actor: IRemoteUser, activity: IObject) { - if (isCollectionOrOrderedCollection(activity)) { - const resolver = new Resolver(); - for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { - const act = await resolver.resolve(item); - try { - await performOneActivity(actor, act); - } catch (e) { - apLogger.error(e); - } - } - } else { - await performOneActivity(actor, activity); - } -} - -async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> { - if (actor.isSuspended) return; - - if (isCreate(activity)) { - await create(actor, activity); - } else if (isDelete(activity)) { - await performDeleteActivity(actor, activity); - } else if (isUpdate(activity)) { - await performUpdateActivity(actor, activity); - } else if (isRead(activity)) { - await performReadActivity(actor, activity); - } else if (isFollow(activity)) { - await follow(actor, activity); - } else if (isAccept(activity)) { - await accept(actor, activity); - } else if (isReject(activity)) { - await reject(actor, activity); - } else if (isAdd(activity)) { - await add(actor, activity).catch(err => apLogger.error(err)); - } else if (isRemove(activity)) { - await remove(actor, activity).catch(err => apLogger.error(err)); - } else if (isAnnounce(activity)) { - await announce(actor, activity); - } else if (isLike(activity)) { - await like(actor, activity); - } else if (isUndo(activity)) { - await undo(actor, activity); - } else if (isBlock(activity)) { - await block(actor, activity); - } else if (isFlag(activity)) { - await flag(actor, activity); - } else { - apLogger.warn(`unrecognized activity type: ${(activity as any).type}`); - } -} diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts deleted file mode 100644 index 58d5aefefc..0000000000 --- a/src/remote/activitypub/kernel/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { ILike, getApId } from '../type'; -import create from '@/services/note/reaction/create'; -import { fetchNote, extractEmojis } from '../models/note'; - -export default async (actor: IRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await extractEmojis(activity.tag || [], actor.host).catch(() => null); - - return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { - return 'skip: already reacted'; - } else { - throw e; - } - }).then(() => 'ok'); -}; diff --git a/src/remote/activitypub/kernel/move/index.ts b/src/remote/activitypub/kernel/move/index.ts deleted file mode 100644 index e69de29bb2..0000000000 --- a/src/remote/activitypub/kernel/move/index.ts +++ /dev/null diff --git a/src/remote/activitypub/kernel/read.ts b/src/remote/activitypub/kernel/read.ts deleted file mode 100644 index 11a1731869..0000000000 --- a/src/remote/activitypub/kernel/read.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { IRead, getApId } from '../type'; -import { isSelfHost, extractDbHost } from '@/misc/convert-host'; -import { MessagingMessages } from '@/models/index'; -import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message'; - -export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => { - const id = await getApId(activity.object); - - if (!isSelfHost(extractDbHost(id))) { - return `skip: Read to foreign host (${id})`; - } - - const messageId = id.split('/').pop(); - - const message = await MessagingMessages.findOne(messageId); - if (message == null) { - return `skip: message not found`; - } - - if (actor.id != message.recipientId) { - return `skip: actor is not a message recipient`; - } - - await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]); - return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; -}; diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts deleted file mode 100644 index 356547440f..0000000000 --- a/src/remote/activitypub/kernel/reject/follow.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import reject from '@/services/following/requests/reject'; -import { IFollow } from '../../type'; -import DbResolver from '../../db-resolver'; -import { relayRejected } from '@/services/relay'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある - - const dbResolver = new DbResolver(); - const follower = await dbResolver.getUserFromApId(activity.actor); - - if (follower == null) { - return `skip: follower not found`; - } - - if (follower.host != null) { - return `skip: follower is not a local user`; - } - - // relay - const match = activity.id?.match(/follow-relay\/(\w+)/); - if (match) { - return await relayRejected(match[1]); - } - - await reject(actor, follower); - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts deleted file mode 100644 index d0de9c329b..0000000000 --- a/src/remote/activitypub/kernel/reject/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Resolver from '../../resolver'; -import { IRemoteUser } from '@/models/entities/user'; -import rejectFollow from './follow'; -import { IReject, isFollow, getApType } from '../../type'; -import { apLogger } from '../../logger'; - -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: IReject): Promise<string> => { - const uri = activity.id || activity; - - logger.info(`Reject: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await rejectFollow(actor, object); - - return `skip: Unknown Reject type: ${getApType(object)}`; -}; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts deleted file mode 100644 index d59953e653..0000000000 --- a/src/remote/activitypub/kernel/remove/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { IRemove } from '../../type'; -import { resolveNote } from '../../models/note'; -import { removePinned } from '@/services/i/pin'; - -export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - if (activity.target == null) { - throw new Error('target is null'); - } - - if (activity.target === actor.featured) { - const note = await resolveNote(activity.object); - if (note == null) throw new Error('note not found'); - await removePinned(actor, note.id); - return; - } - - throw new Error(`unknown target: ${activity.target}`); -}; diff --git a/src/remote/activitypub/kernel/undo/announce.ts b/src/remote/activitypub/kernel/undo/announce.ts deleted file mode 100644 index 7bb9d7fcad..0000000000 --- a/src/remote/activitypub/kernel/undo/announce.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Notes } from '@/models/index'; -import { IRemoteUser } from '@/models/entities/user'; -import { IAnnounce, getApId } from '../../type'; -import deleteNote from '@/services/note/delete'; - -export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => { - const uri = getApId(activity); - - const note = await Notes.findOne({ - uri - }); - - if (!note) return 'skip: no such Announce'; - - await deleteNote(actor, note); - return 'ok: deleted'; -}; diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts deleted file mode 100644 index 61940486be..0000000000 --- a/src/remote/activitypub/kernel/undo/block.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IBlock } from '../../type'; -import unblock from '@/services/blocking/delete'; -import { IRemoteUser } from '@/models/entities/user'; -import DbResolver from '../../db-resolver'; - -export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { - const dbResolver = new DbResolver(); - const blockee = await dbResolver.getUserFromApId(activity.object); - - if (blockee == null) { - return `skip: blockee not found`; - } - - if (blockee.host != null) { - return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; - } - - await unblock(actor, blockee); - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts deleted file mode 100644 index d85c7e4a71..0000000000 --- a/src/remote/activitypub/kernel/undo/follow.ts +++ /dev/null @@ -1,41 +0,0 @@ -import unfollow from '@/services/following/delete'; -import cancelRequest from '@/services/following/requests/cancel'; -import { IFollow } from '../../type'; -import { IRemoteUser } from '@/models/entities/user'; -import { FollowRequests, Followings } from '@/models/index'; -import DbResolver from '../../db-resolver'; - -export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { - const dbResolver = new DbResolver(); - - const followee = await dbResolver.getUserFromApId(activity.object); - if (followee == null) { - return `skip: followee not found`; - } - - if (followee.host != null) { - return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`; - } - - const req = await FollowRequests.findOne({ - followerId: actor.id, - followeeId: followee.id - }); - - const following = await Followings.findOne({ - followerId: actor.id, - followeeId: followee.id - }); - - if (req) { - await cancelRequest(followee, actor); - return `ok: follow request canceled`; - } - - if (following) { - await unfollow(actor, followee); - return `ok: unfollowed`; - } - - return `skip: リクエストもフォローもされていない`; -}; diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts deleted file mode 100644 index 14b1add152..0000000000 --- a/src/remote/activitypub/kernel/undo/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type'; -import unfollow from './follow'; -import unblock from './block'; -import undoLike from './like'; -import { undoAnnounce } from './announce'; -import Resolver from '../../resolver'; -import { apLogger } from '../../logger'; - -const logger = apLogger; - -export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - logger.info(`Undo: ${uri}`); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - logger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isFollow(object)) return await unfollow(actor, object); - if (isBlock(object)) return await unblock(actor, object); - if (isLike(object)) return await undoLike(actor, object); - if (isAnnounce(object)) return await undoAnnounce(actor, object); - - return `skip: unknown object type ${getApType(object)}`; -}; diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts deleted file mode 100644 index 107d3053e3..0000000000 --- a/src/remote/activitypub/kernel/undo/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { ILike, getApId } from '../../type'; -import deleteReaction from '@/services/note/reaction/delete'; -import { fetchNote } from '../../models/note'; - -/** - * Process Undo.Like activity - */ -export default async (actor: IRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await deleteReaction(actor, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return; - throw e; - }); - - return `ok`; -}; diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts deleted file mode 100644 index 52bfc5002e..0000000000 --- a/src/remote/activitypub/kernel/update/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IRemoteUser } from '@/models/entities/user'; -import { getApType, IUpdate, isActor } from '../../type'; -import { apLogger } from '../../logger'; -import { updateQuestion } from '../../models/question'; -import Resolver from '../../resolver'; -import { updatePerson } from '../../models/person'; - -/** - * Updateアクティビティを捌きます - */ -export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => { - if ('actor' in activity && actor.uri !== activity.actor) { - return `skip: invalid actor`; - } - - apLogger.debug('Update'); - - const resolver = new Resolver(); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isActor(object)) { - await updatePerson(actor.uri!, resolver, object); - return `ok: Person updated`; - } else if (getApType(object) === 'Question') { - await updateQuestion(object).catch(e => console.log(e)); - return `ok: Question updated`; - } else { - return `skip: Unknown type: ${getApType(object)}`; - } -}; diff --git a/src/remote/activitypub/logger.ts b/src/remote/activitypub/logger.ts deleted file mode 100644 index e13add01db..0000000000 --- a/src/remote/activitypub/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { remoteLogger } from '../logger'; - -export const apLogger = remoteLogger.createSubLogger('ap', 'magenta'); diff --git a/src/remote/activitypub/misc/contexts.ts b/src/remote/activitypub/misc/contexts.ts deleted file mode 100644 index 1426ba15f5..0000000000 --- a/src/remote/activitypub/misc/contexts.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* tslint:disable:quotemark indent */ -const id_v1 = { - "@context": { - "id": "@id", - "type": "@type", - - "cred": "https://w3id.org/credentials#", - "dc": "http://purl.org/dc/terms/", - "identity": "https://w3id.org/identity#", - "perm": "https://w3id.org/permissions#", - "ps": "https://w3id.org/payswarm#", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "sec": "https://w3id.org/security#", - "schema": "http://schema.org/", - "xsd": "http://www.w3.org/2001/XMLSchema#", - - "Group": "https://www.w3.org/ns/activitystreams#Group", - - "claim": {"@id": "cred:claim", "@type": "@id"}, - "credential": {"@id": "cred:credential", "@type": "@id"}, - "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, - "issuer": {"@id": "cred:issuer", "@type": "@id"}, - "recipient": {"@id": "cred:recipient", "@type": "@id"}, - "Credential": "cred:Credential", - "CryptographicKeyCredential": "cred:CryptographicKeyCredential", - - "about": {"@id": "schema:about", "@type": "@id"}, - "address": {"@id": "schema:address", "@type": "@id"}, - "addressCountry": "schema:addressCountry", - "addressLocality": "schema:addressLocality", - "addressRegion": "schema:addressRegion", - "comment": "rdfs:comment", - "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, - "creator": {"@id": "dc:creator", "@type": "@id"}, - "description": "schema:description", - "email": "schema:email", - "familyName": "schema:familyName", - "givenName": "schema:givenName", - "image": {"@id": "schema:image", "@type": "@id"}, - "label": "rdfs:label", - "name": "schema:name", - "postalCode": "schema:postalCode", - "streetAddress": "schema:streetAddress", - "title": "dc:title", - "url": {"@id": "schema:url", "@type": "@id"}, - "Person": "schema:Person", - "PostalAddress": "schema:PostalAddress", - "Organization": "schema:Organization", - - "identityService": {"@id": "identity:identityService", "@type": "@id"}, - "idp": {"@id": "identity:idp", "@type": "@id"}, - "Identity": "identity:Identity", - - "paymentProcessor": "ps:processor", - "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, - - "cipherAlgorithm": "sec:cipherAlgorithm", - "cipherData": "sec:cipherData", - "cipherKey": "sec:cipherKey", - "digestAlgorithm": "sec:digestAlgorithm", - "digestValue": "sec:digestValue", - "domain": "sec:domain", - "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "initializationVector": "sec:initializationVector", - "member": {"@id": "schema:member", "@type": "@id"}, - "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, - "nonce": "sec:nonce", - "normalizationAlgorithm": "sec:normalizationAlgorithm", - "owner": {"@id": "sec:owner", "@type": "@id"}, - "password": "sec:password", - "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, - "privateKeyPem": "sec:privateKeyPem", - "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, - "publicKeyPem": "sec:publicKeyPem", - "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, - "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, - "signature": "sec:signature", - "signatureAlgorithm": "sec:signatureAlgorithm", - "signatureValue": "sec:signatureValue", - "CryptographicKey": "sec:Key", - "EncryptedMessage": "sec:EncryptedMessage", - "GraphSignature2012": "sec:GraphSignature2012", - "LinkedDataSignature2015": "sec:LinkedDataSignature2015", - - "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, - "writePermission": {"@id": "perm:writePermission", "@type": "@id"} - } -}; - -const security_v1 = { - "@context": { - "id": "@id", - "type": "@type", - - "dc": "http://purl.org/dc/terms/", - "sec": "https://w3id.org/security#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - - "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", - "Ed25519Signature2018": "sec:Ed25519Signature2018", - "EncryptedMessage": "sec:EncryptedMessage", - "GraphSignature2012": "sec:GraphSignature2012", - "LinkedDataSignature2015": "sec:LinkedDataSignature2015", - "LinkedDataSignature2016": "sec:LinkedDataSignature2016", - "CryptographicKey": "sec:Key", - - "authenticationTag": "sec:authenticationTag", - "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", - "cipherAlgorithm": "sec:cipherAlgorithm", - "cipherData": "sec:cipherData", - "cipherKey": "sec:cipherKey", - "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, - "creator": {"@id": "dc:creator", "@type": "@id"}, - "digestAlgorithm": "sec:digestAlgorithm", - "digestValue": "sec:digestValue", - "domain": "sec:domain", - "encryptionKey": "sec:encryptionKey", - "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "initializationVector": "sec:initializationVector", - "iterationCount": "sec:iterationCount", - "nonce": "sec:nonce", - "normalizationAlgorithm": "sec:normalizationAlgorithm", - "owner": {"@id": "sec:owner", "@type": "@id"}, - "password": "sec:password", - "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, - "privateKeyPem": "sec:privateKeyPem", - "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, - "publicKeyBase58": "sec:publicKeyBase58", - "publicKeyPem": "sec:publicKeyPem", - "publicKeyWif": "sec:publicKeyWif", - "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, - "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, - "salt": "sec:salt", - "signature": "sec:signature", - "signatureAlgorithm": "sec:signingAlgorithm", - "signatureValue": "sec:signatureValue" - } -}; - -const activitystreams = { - "@context": { - "@vocab": "_:", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "as": "https://www.w3.org/ns/activitystreams#", - "ldp": "http://www.w3.org/ns/ldp#", - "vcard": "http://www.w3.org/2006/vcard/ns#", - "id": "@id", - "type": "@type", - "Accept": "as:Accept", - "Activity": "as:Activity", - "IntransitiveActivity": "as:IntransitiveActivity", - "Add": "as:Add", - "Announce": "as:Announce", - "Application": "as:Application", - "Arrive": "as:Arrive", - "Article": "as:Article", - "Audio": "as:Audio", - "Block": "as:Block", - "Collection": "as:Collection", - "CollectionPage": "as:CollectionPage", - "Relationship": "as:Relationship", - "Create": "as:Create", - "Delete": "as:Delete", - "Dislike": "as:Dislike", - "Document": "as:Document", - "Event": "as:Event", - "Follow": "as:Follow", - "Flag": "as:Flag", - "Group": "as:Group", - "Ignore": "as:Ignore", - "Image": "as:Image", - "Invite": "as:Invite", - "Join": "as:Join", - "Leave": "as:Leave", - "Like": "as:Like", - "Link": "as:Link", - "Mention": "as:Mention", - "Note": "as:Note", - "Object": "as:Object", - "Offer": "as:Offer", - "OrderedCollection": "as:OrderedCollection", - "OrderedCollectionPage": "as:OrderedCollectionPage", - "Organization": "as:Organization", - "Page": "as:Page", - "Person": "as:Person", - "Place": "as:Place", - "Profile": "as:Profile", - "Question": "as:Question", - "Reject": "as:Reject", - "Remove": "as:Remove", - "Service": "as:Service", - "TentativeAccept": "as:TentativeAccept", - "TentativeReject": "as:TentativeReject", - "Tombstone": "as:Tombstone", - "Undo": "as:Undo", - "Update": "as:Update", - "Video": "as:Video", - "View": "as:View", - "Listen": "as:Listen", - "Read": "as:Read", - "Move": "as:Move", - "Travel": "as:Travel", - "IsFollowing": "as:IsFollowing", - "IsFollowedBy": "as:IsFollowedBy", - "IsContact": "as:IsContact", - "IsMember": "as:IsMember", - "subject": { - "@id": "as:subject", - "@type": "@id" - }, - "relationship": { - "@id": "as:relationship", - "@type": "@id" - }, - "actor": { - "@id": "as:actor", - "@type": "@id" - }, - "attributedTo": { - "@id": "as:attributedTo", - "@type": "@id" - }, - "attachment": { - "@id": "as:attachment", - "@type": "@id" - }, - "bcc": { - "@id": "as:bcc", - "@type": "@id" - }, - "bto": { - "@id": "as:bto", - "@type": "@id" - }, - "cc": { - "@id": "as:cc", - "@type": "@id" - }, - "context": { - "@id": "as:context", - "@type": "@id" - }, - "current": { - "@id": "as:current", - "@type": "@id" - }, - "first": { - "@id": "as:first", - "@type": "@id" - }, - "generator": { - "@id": "as:generator", - "@type": "@id" - }, - "icon": { - "@id": "as:icon", - "@type": "@id" - }, - "image": { - "@id": "as:image", - "@type": "@id" - }, - "inReplyTo": { - "@id": "as:inReplyTo", - "@type": "@id" - }, - "items": { - "@id": "as:items", - "@type": "@id" - }, - "instrument": { - "@id": "as:instrument", - "@type": "@id" - }, - "orderedItems": { - "@id": "as:items", - "@type": "@id", - "@container": "@list" - }, - "last": { - "@id": "as:last", - "@type": "@id" - }, - "location": { - "@id": "as:location", - "@type": "@id" - }, - "next": { - "@id": "as:next", - "@type": "@id" - }, - "object": { - "@id": "as:object", - "@type": "@id" - }, - "oneOf": { - "@id": "as:oneOf", - "@type": "@id" - }, - "anyOf": { - "@id": "as:anyOf", - "@type": "@id" - }, - "closed": { - "@id": "as:closed", - "@type": "xsd:dateTime" - }, - "origin": { - "@id": "as:origin", - "@type": "@id" - }, - "accuracy": { - "@id": "as:accuracy", - "@type": "xsd:float" - }, - "prev": { - "@id": "as:prev", - "@type": "@id" - }, - "preview": { - "@id": "as:preview", - "@type": "@id" - }, - "replies": { - "@id": "as:replies", - "@type": "@id" - }, - "result": { - "@id": "as:result", - "@type": "@id" - }, - "audience": { - "@id": "as:audience", - "@type": "@id" - }, - "partOf": { - "@id": "as:partOf", - "@type": "@id" - }, - "tag": { - "@id": "as:tag", - "@type": "@id" - }, - "target": { - "@id": "as:target", - "@type": "@id" - }, - "to": { - "@id": "as:to", - "@type": "@id" - }, - "url": { - "@id": "as:url", - "@type": "@id" - }, - "altitude": { - "@id": "as:altitude", - "@type": "xsd:float" - }, - "content": "as:content", - "contentMap": { - "@id": "as:content", - "@container": "@language" - }, - "name": "as:name", - "nameMap": { - "@id": "as:name", - "@container": "@language" - }, - "duration": { - "@id": "as:duration", - "@type": "xsd:duration" - }, - "endTime": { - "@id": "as:endTime", - "@type": "xsd:dateTime" - }, - "height": { - "@id": "as:height", - "@type": "xsd:nonNegativeInteger" - }, - "href": { - "@id": "as:href", - "@type": "@id" - }, - "hreflang": "as:hreflang", - "latitude": { - "@id": "as:latitude", - "@type": "xsd:float" - }, - "longitude": { - "@id": "as:longitude", - "@type": "xsd:float" - }, - "mediaType": "as:mediaType", - "published": { - "@id": "as:published", - "@type": "xsd:dateTime" - }, - "radius": { - "@id": "as:radius", - "@type": "xsd:float" - }, - "rel": "as:rel", - "startIndex": { - "@id": "as:startIndex", - "@type": "xsd:nonNegativeInteger" - }, - "startTime": { - "@id": "as:startTime", - "@type": "xsd:dateTime" - }, - "summary": "as:summary", - "summaryMap": { - "@id": "as:summary", - "@container": "@language" - }, - "totalItems": { - "@id": "as:totalItems", - "@type": "xsd:nonNegativeInteger" - }, - "units": "as:units", - "updated": { - "@id": "as:updated", - "@type": "xsd:dateTime" - }, - "width": { - "@id": "as:width", - "@type": "xsd:nonNegativeInteger" - }, - "describes": { - "@id": "as:describes", - "@type": "@id" - }, - "formerType": { - "@id": "as:formerType", - "@type": "@id" - }, - "deleted": { - "@id": "as:deleted", - "@type": "xsd:dateTime" - }, - "inbox": { - "@id": "ldp:inbox", - "@type": "@id" - }, - "outbox": { - "@id": "as:outbox", - "@type": "@id" - }, - "following": { - "@id": "as:following", - "@type": "@id" - }, - "followers": { - "@id": "as:followers", - "@type": "@id" - }, - "streams": { - "@id": "as:streams", - "@type": "@id" - }, - "preferredUsername": "as:preferredUsername", - "endpoints": { - "@id": "as:endpoints", - "@type": "@id" - }, - "uploadMedia": { - "@id": "as:uploadMedia", - "@type": "@id" - }, - "proxyUrl": { - "@id": "as:proxyUrl", - "@type": "@id" - }, - "liked": { - "@id": "as:liked", - "@type": "@id" - }, - "oauthAuthorizationEndpoint": { - "@id": "as:oauthAuthorizationEndpoint", - "@type": "@id" - }, - "oauthTokenEndpoint": { - "@id": "as:oauthTokenEndpoint", - "@type": "@id" - }, - "provideClientKey": { - "@id": "as:provideClientKey", - "@type": "@id" - }, - "signClientKey": { - "@id": "as:signClientKey", - "@type": "@id" - }, - "sharedInbox": { - "@id": "as:sharedInbox", - "@type": "@id" - }, - "Public": { - "@id": "as:Public", - "@type": "@id" - }, - "source": "as:source", - "likes": { - "@id": "as:likes", - "@type": "@id" - }, - "shares": { - "@id": "as:shares", - "@type": "@id" - }, - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - } - } -}; - -export const CONTEXTS: Record<string, any> = { - "https://w3id.org/identity/v1": id_v1, - "https://w3id.org/security/v1": security_v1, - "https://www.w3.org/ns/activitystreams": activitystreams, -}; diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts deleted file mode 100644 index a24ec43a69..0000000000 --- a/src/remote/activitypub/misc/get-note-html.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as mfm from 'mfm-js'; -import { fnNameList } from '@/mfm/fn-name-list'; -import { Note } from '@/models/entities/note'; -import { toHtml } from '../../../mfm/to-html'; - -export default function(note: Note) { - let html = note.text ? toHtml(mfm.parse(note.text, { fnNameList }), JSON.parse(note.mentionedRemoteUsers)) : null; - if (html == null) html = '<p>.</p>'; - - return html; -} diff --git a/src/remote/activitypub/misc/html-to-mfm.ts b/src/remote/activitypub/misc/html-to-mfm.ts deleted file mode 100644 index 5cca04df21..0000000000 --- a/src/remote/activitypub/misc/html-to-mfm.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IObject } from '../type'; -import { extractApHashtagObjects } from '../models/tag'; -import { fromHtml } from '../../../mfm/from-html'; - -export function htmlToMfm(html: string, tag?: IObject | IObject[]) { - const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); - - return fromHtml(html, hashtagNames); -} diff --git a/src/remote/activitypub/misc/ld-signature.ts b/src/remote/activitypub/misc/ld-signature.ts deleted file mode 100644 index dec07ea81b..0000000000 --- a/src/remote/activitypub/misc/ld-signature.ts +++ /dev/null @@ -1,134 +0,0 @@ -import * as crypto from 'crypto'; -import * as jsonld from 'jsonld'; -import { CONTEXTS } from './contexts'; -import fetch from 'node-fetch'; -import { httpAgent, httpsAgent } from '@/misc/fetch'; - -// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 - -export class LdSignature { - public debug = false; - public preLoad = true; - public loderTimeout = 10 * 1000; - - constructor() { - } - - public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> { - const options = { - type: 'RsaSignature2017', - creator, - domain, - nonce: crypto.randomBytes(16).toString('hex'), - created: (created || new Date()).toISOString() - } as { - type: string; - creator: string; - domain: string; - nonce: string; - created: string; - }; - - if (!domain) { - delete options.domain; - } - - const toBeSigned = await this.createVerifyData(data, options); - - const signer = crypto.createSign('sha256'); - signer.update(toBeSigned); - signer.end(); - - const signature = signer.sign(privateKey); - - return { - ...data, - signature: { - ...options, - signatureValue: signature.toString('base64') - } - }; - } - - public async verifyRsaSignature2017(data: any, publicKey: string): Promise<boolean> { - const toBeSigned = await this.createVerifyData(data, data.signature); - const verifier = crypto.createVerify('sha256'); - verifier.update(toBeSigned); - return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); - } - - public async createVerifyData(data: any, options: any) { - const transformedOptions = { - ...options, - '@context': 'https://w3id.org/identity/v1' - }; - delete transformedOptions['type']; - delete transformedOptions['id']; - delete transformedOptions['signatureValue']; - const canonizedOptions = await this.normalize(transformedOptions); - const optionsHash = this.sha256(canonizedOptions); - const transformedData = { ...data }; - delete transformedData['signature']; - const cannonidedData = await this.normalize(transformedData); - if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); - const documentHash = this.sha256(cannonidedData); - const verifyData = `${optionsHash}${documentHash}`; - return verifyData; - } - - public async normalize(data: any) { - const customLoader = this.getLoader(); - return await jsonld.normalize(data, { - documentLoader: customLoader - }); - } - - private getLoader() { - return async (url: string): Promise<any> => { - if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; - - if (this.preLoad) { - if (url in CONTEXTS) { - if (this.debug) console.debug(`HIT: ${url}`); - return { - contextUrl: null, - document: CONTEXTS[url], - documentUrl: url - }; - } - } - - if (this.debug) console.debug(`MISS: ${url}`); - const document = await this.fetchDocument(url); - return { - contextUrl: null, - document: document, - documentUrl: url - }; - }; - } - - private async fetchDocument(url: string) { - const json = await fetch(url, { - headers: { - Accept: 'application/ld+json, application/json', - }, - timeout: this.loderTimeout, - agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent, - }).then(res => { - if (!res.ok) { - throw `${res.status} ${res.statusText}`; - } else { - return res.json(); - } - }); - - return json; - } - - public sha256(data: string): string { - const hash = crypto.createHash('sha256'); - hash.update(data); - return hash.digest('hex'); - } -} 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); -} diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts deleted file mode 100644 index 01f0e3676e..0000000000 --- a/src/remote/activitypub/perform.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IObject } from './type'; -import { IRemoteUser } from '@/models/entities/user'; -import { performActivity } from './kernel/index'; - -export default async (actor: IRemoteUser, activity: IObject): Promise<void> => { - await performActivity(actor, activity); -}; diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts deleted file mode 100644 index f1e61f4c6a..0000000000 --- a/src/remote/activitypub/renderer/accept.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; - -export default (object: any, user: { id: User['id']; host: null }) => ({ - type: 'Accept', - actor: `${config.url}/users/${user.id}`, - object -}); diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts deleted file mode 100644 index 21414a9380..0000000000 --- a/src/remote/activitypub/renderer/add.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index'; -import { ILocalUser } from '@/models/entities/user'; - -export default (user: ILocalUser, target: any, object: any) => ({ - type: 'Add', - actor: `${config.url}/users/${user.id}`, - target, - object -}); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts deleted file mode 100644 index 7bf90922be..0000000000 --- a/src/remote/activitypub/renderer/announce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import config from '@/config/index'; -import { Note } from '@/models/entities/note'; - -export default (object: any, note: Note) => { - const attributedTo = `${config.url}/users/${note.userId}`; - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`]; - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public']; - } else { - return null; - } - - return { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: 'Announce', - published: note.createdAt.toISOString(), - to, - cc, - object - }; -}; diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts deleted file mode 100644 index bb3d74295a..0000000000 --- a/src/remote/activitypub/renderer/block.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index'; -import { ILocalUser, IRemoteUser } from '@/models/entities/user'; - -export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ - type: 'Block', - actor: `${config.url}/users/${blocker.id}`, - object: blockee.uri -}); diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts deleted file mode 100644 index 04aa993a91..0000000000 --- a/src/remote/activitypub/renderer/create.ts +++ /dev/null @@ -1,17 +0,0 @@ -import config from '@/config/index'; -import { Note } from '@/models/entities/note'; - -export default (object: any, note: Note) => { - const activity = { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: 'Create', - published: note.createdAt.toISOString(), - object - } as any; - - if (object.to) activity.to = object.to; - if (object.cc) activity.cc = object.cc; - - return activity; -}; diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts deleted file mode 100644 index 176a6f7e27..0000000000 --- a/src/remote/activitypub/renderer/delete.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; - -export default (object: any, user: { id: User['id']; host: null }) => ({ - type: 'Delete', - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), -}); diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts deleted file mode 100644 index a9d86dea15..0000000000 --- a/src/remote/activitypub/renderer/document.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file'; -import { DriveFiles } from '@/models/index'; - -export default (file: DriveFile) => ({ - type: 'Document', - mediaType: file.type, - url: DriveFiles.getPublicUrl(file), - name: file.comment, -}); diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts deleted file mode 100644 index ca514c56b5..0000000000 --- a/src/remote/activitypub/renderer/emoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index'; -import { Emoji } from '@/models/entities/emoji'; - -export default (emoji: Emoji) => ({ - id: `${config.url}/emojis/${emoji.name}`, - type: 'Emoji', - name: `:${emoji.name}:`, - updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString, - icon: { - type: 'Image', - mediaType: emoji.type || 'image/png', - url: emoji.url - } -}); diff --git a/src/remote/activitypub/renderer/follow-relay.ts b/src/remote/activitypub/renderer/follow-relay.ts deleted file mode 100644 index 984c3c7639..0000000000 --- a/src/remote/activitypub/renderer/follow-relay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index'; -import { Relay } from '@/models/entities/relay'; -import { ILocalUser } from '@/models/entities/user'; - -export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) { - const follow = { - id: `${config.url}/activities/follow-relay/${relay.id}`, - type: 'Follow', - actor: `${config.url}/users/${relayActor.id}`, - object: 'https://www.w3.org/ns/activitystreams#Public' - }; - - return follow; -} diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts deleted file mode 100644 index e3dde7f7fe..0000000000 --- a/src/remote/activitypub/renderer/follow-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from '@/config/index'; -import { Users } from '@/models/index'; -import { User } from '@/models/entities/user'; - -/** - * Convert (local|remote)(Follower|Followee)ID to URL - * @param id Follower|Followee ID - */ -export default async function renderFollowUser(id: User['id']): Promise<any> { - const user = await Users.findOneOrFail(id); - return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; -} diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts deleted file mode 100644 index c8a7946799..0000000000 --- a/src/remote/activitypub/renderer/follow.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; -import { Users } from '@/models/index'; - -export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { - const follow = { - type: 'Follow', - actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, - object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri - } as any; - - if (requestId) follow.id = requestId; - - return follow; -}; diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts deleted file mode 100644 index 290c74c7fe..0000000000 --- a/src/remote/activitypub/renderer/hashtag.ts +++ /dev/null @@ -1,7 +0,0 @@ -import config from '@/config/index'; - -export default (tag: string) => ({ - type: 'Hashtag', - href: `${config.url}/tags/${encodeURIComponent(tag)}`, - name: `#${tag}` -}); diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts deleted file mode 100644 index 0cb3d6ed65..0000000000 --- a/src/remote/activitypub/renderer/image.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file'; -import { DriveFiles } from '@/models/index'; - -export default (file: DriveFile) => ({ - type: 'Image', - url: DriveFiles.getPublicUrl(file), - sensitive: file.isSensitive, - name: file.comment -}); diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts deleted file mode 100644 index f6ec6583d0..0000000000 --- a/src/remote/activitypub/renderer/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import config from '@/config/index'; -import { v4 as uuid } from 'uuid'; -import { IActivity } from '../type'; -import { LdSignature } from '../misc/ld-signature'; -import { getUserKeypair } from '@/misc/keypair-store'; -import { User } from '@/models/entities/user'; - -export const renderActivity = (x: any): IActivity | null => { - if (x == null) return null; - - if (x !== null && typeof x === 'object' && x.id == null) { - x.id = `${config.url}/${uuid()}`; - } - - return Object.assign({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - // as non-standards - manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - // schema - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value', - // Misskey - misskey: `${config.url}/ns#`, - '_misskey_content': 'misskey:_misskey_content', - '_misskey_quote': 'misskey:_misskey_quote', - '_misskey_reaction': 'misskey:_misskey_reaction', - '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_talk': 'misskey:_misskey_talk', - 'isCat': 'misskey:isCat', - // vcard - vcard: 'http://www.w3.org/2006/vcard/ns#', - } - ] - }, x); -}; - -export const attachLdSignature = async (activity: any, user: { id: User['id']; host: null; }): Promise<IActivity | null> => { - if (activity == null) return null; - - const keypair = await getUserKeypair(user.id); - - const ldSignature = new LdSignature(); - ldSignature.debug = false; - activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`); - - return activity; -}; diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts deleted file mode 100644 index 992f98d79a..0000000000 --- a/src/remote/activitypub/renderer/key.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index'; -import { ILocalUser } from '@/models/entities/user'; -import { UserKeypair } from '@/models/entities/user-keypair'; -import { createPublicKey } from 'crypto'; - -export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({ - id: `${config.url}/users/${user.id}${postfix || '/publickey'}`, - type: 'Key', - owner: `${config.url}/users/${user.id}`, - publicKeyPem: createPublicKey(key.publicKey).export({ - type: 'spki', - format: 'pem' - }) -}); diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts deleted file mode 100644 index a7e79a176f..0000000000 --- a/src/remote/activitypub/renderer/like.ts +++ /dev/null @@ -1,30 +0,0 @@ -import config from '@/config/index'; -import { NoteReaction } from '@/models/entities/note-reaction'; -import { Note } from '@/models/entities/note'; -import { Emojis } from '@/models/index'; -import renderEmoji from './emoji'; - -export const renderLike = async (noteReaction: NoteReaction, note: Note) => { - const reaction = noteReaction.reaction; - - const object = { - type: 'Like', - id: `${config.url}/likes/${noteReaction.id}`, - actor: `${config.url}/users/${noteReaction.userId}`, - object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`, - content: reaction, - _misskey_reaction: reaction - } as any; - - if (reaction.startsWith(':')) { - const name = reaction.replace(/:/g, ''); - const emoji = await Emojis.findOne({ - name, - host: null - }); - - if (emoji) object.tag = [ renderEmoji(emoji) ]; - } - - return object; -}; diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts deleted file mode 100644 index 06d2d33e59..0000000000 --- a/src/remote/activitypub/renderer/mention.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index'; -import { User, ILocalUser } from '@/models/entities/user'; -import { Users } from '@/models/index'; - -export default (mention: User) => ({ - type: 'Mention', - href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`, - name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, -}); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts deleted file mode 100644 index 84a1786784..0000000000 --- a/src/remote/activitypub/renderer/note.ts +++ /dev/null @@ -1,168 +0,0 @@ -import renderDocument from './document'; -import renderHashtag from './hashtag'; -import renderMention from './mention'; -import renderEmoji from './emoji'; -import config from '@/config/index'; -import toHtml from '../misc/get-note-html'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note'; -import { DriveFile } from '@/models/entities/drive-file'; -import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index'; -import { In } from 'typeorm'; -import { Emoji } from '@/models/entities/emoji'; -import { Poll } from '@/models/entities/poll'; - -export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<any> { - const getPromisedFiles = async (ids: string[]) => { - if (!ids || ids.length === 0) return []; - const items = await DriveFiles.find({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; - }; - - let inReplyTo; - let inReplyToNote: Note | undefined; - - if (note.replyId) { - inReplyToNote = await Notes.findOne(note.replyId); - - if (inReplyToNote != null) { - const inReplyToUser = await Users.findOne(inReplyToNote.userId); - - if (inReplyToUser != null) { - if (inReplyToNote.uri) { - inReplyTo = inReplyToNote.uri; - } else { - if (dive) { - inReplyTo = await renderNote(inReplyToNote, false); - } else { - inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; - } - } - } - } - } else { - inReplyTo = null; - } - - let quote; - - if (note.renoteId) { - const renote = await Notes.findOne(note.renoteId); - - if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; - } - } - - const user = await Users.findOneOrFail(note.userId); - - const attributedTo = `${config.url}/users/${user.id}`; - - const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`].concat(mentions); - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); - } else if (note.visibility === 'followers') { - to = [`${attributedTo}/followers`]; - cc = mentions; - } else { - to = mentions; - } - - const mentionedUsers = note.mentions.length > 0 ? await Users.find({ - id: In(note.mentions) - }) : []; - - const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); - const mentionTags = mentionedUsers.map(u => renderMention(u)); - - const files = await getPromisedFiles(note.fileIds); - - const text = note.text; - let poll: Poll | undefined; - - if (note.hasPoll) { - poll = await Polls.findOne({ noteId: note.id }); - } - - let apText = text; - if (apText == null) apText = ''; - - if (quote) { - apText += `\n\nRE: ${quote}`; - } - - const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - - const content = toHtml(Object.assign({}, note, { - text: apText - })); - - const emojis = await getEmojis(note.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const tag = [ - ...hashtagTags, - ...mentionTags, - ...apemojis, - ]; - - const asPoll = poll ? { - type: 'Question', - content: toHtml(Object.assign({}, note, { - text: text - })), - [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, - [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ - type: 'Note', - name: text, - replies: { - type: 'Collection', - totalItems: poll!.votes[i] - } - })) - } : {}; - - const asTalk = isTalk ? { - _misskey_talk: true - } : {}; - - return { - id: `${config.url}/notes/${note.id}`, - type: 'Note', - attributedTo, - summary, - content, - _misskey_content: text, - _misskey_quote: quote, - quoteUrl: quote, - published: note.createdAt.toISOString(), - to, - cc, - inReplyTo, - attachment: files.map(renderDocument), - sensitive: note.cw != null || files.some(file => file.isSensitive), - tag, - ...asPoll, - ...asTalk - }; -} - -export async function getEmojis(names: string[]): Promise<Emoji[]> { - if (names == null || names.length === 0) return []; - - const emojis = await Promise.all( - names.map(name => Emojis.findOne({ - name, - host: null - })) - ); - - return emojis.filter(emoji => emoji != null) as Emoji[]; -} diff --git a/src/remote/activitypub/renderer/ordered-collection-page.ts b/src/remote/activitypub/renderer/ordered-collection-page.ts deleted file mode 100644 index 2433358646..0000000000 --- a/src/remote/activitypub/renderer/ordered-collection-page.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Render OrderedCollectionPage - * @param id URL of self - * @param totalItems Number of total items - * @param orderedItems Items - * @param partOf URL of base - * @param prev URL of prev page (optional) - * @param next URL of next page (optional) - */ -export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page = { - id, - partOf, - type: 'OrderedCollectionPage', - totalItems, - orderedItems - } as any; - - if (prev) page.prev = prev; - if (next) page.next = next; - - return page; -} diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts deleted file mode 100644 index 68870a0ecd..0000000000 --- a/src/remote/activitypub/renderer/ordered-collection.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Render OrderedCollection - * @param id URL of self - * @param totalItems Total number of items - * @param first URL of first page (optional) - * @param last URL of last page (optional) - * @param orderedItems attached objects (optional) - */ -export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: object) { - const page: any = { - id, - type: 'OrderedCollection', - totalItems, - }; - - if (first) page.first = first; - if (last) page.last = last; - if (orderedItems) page.orderedItems = orderedItems; - - return page; -} diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts deleted file mode 100644 index 7e94abddfc..0000000000 --- a/src/remote/activitypub/renderer/person.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { URL } from 'url'; -import * as mfm from 'mfm-js'; -import renderImage from './image'; -import renderKey from './key'; -import config from '@/config/index'; -import { ILocalUser } from '@/models/entities/user'; -import { toHtml } from '../../../mfm/to-html'; -import { getEmojis } from './note'; -import renderEmoji from './emoji'; -import { IIdentifier } from '../models/identifier'; -import renderHashtag from './hashtag'; -import { DriveFiles, UserProfiles } from '@/models/index'; -import { getUserKeypair } from '@/misc/keypair-store'; -import { fnNameList } from '@/mfm/fn-name-list'; - -export async function renderPerson(user: ILocalUser) { - const id = `${config.url}/users/${user.id}`; - const isSystem = !!user.username.match(/\./); - - const [avatar, banner, profile] = await Promise.all([ - user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined), - user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined), - UserProfiles.findOneOrFail(user.id) - ]); - - const attachment: { - type: 'PropertyValue', - name: string, - value: string, - identifier?: IIdentifier - }[] = []; - - if (profile.fields) { - for (const field of profile.fields) { - attachment.push({ - type: 'PropertyValue', - name: field.name, - value: (field.value != null && field.value.match(/^https?:/)) - ? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>` - : field.value - }); - } - } - - const emojis = await getEmojis(user.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag)); - - const tag = [ - ...apemojis, - ...hashtagTags, - ]; - - const keypair = await getUserKeypair(user.id); - - const person = { - type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', - id, - inbox: `${id}/inbox`, - outbox: `${id}/outbox`, - followers: `${id}/followers`, - following: `${id}/following`, - featured: `${id}/collections/featured`, - sharedInbox: `${config.url}/inbox`, - endpoints: { sharedInbox: `${config.url}/inbox` }, - url: `${config.url}/@${user.username}`, - preferredUsername: user.username, - name: user.name, - summary: profile.description ? toHtml(mfm.parse(profile.description, { fnNameList })) : null, - icon: avatar ? renderImage(avatar) : null, - image: banner ? renderImage(banner) : null, - tag, - manuallyApprovesFollowers: user.isLocked, - discoverable: !!user.isExplorable, - publicKey: renderKey(user, keypair, `#main-key`), - isCat: user.isCat, - attachment: attachment.length ? attachment : undefined - } as any; - - if (profile?.birthday) { - person['vcard:bday'] = profile.birthday; - } - - if (profile?.location) { - person['vcard:Address'] = profile.location; - } - - return person; -} diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts deleted file mode 100644 index 246d599bab..0000000000 --- a/src/remote/activitypub/renderer/question.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; -import { Note } from '@/models/entities/note'; -import { Poll } from '@/models/entities/poll'; - -export default async function renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { - const question = { - type: 'Question', - id: `${config.url}/questions/${note.id}`, - actor: `${config.url}/users/${user.id}`, - content: note.text || '', - [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ - name: text, - _misskey_votes: poll.votes[i], - replies: { - type: 'Collection', - totalItems: poll.votes[i] - } - })) - }; - - return question; -} diff --git a/src/remote/activitypub/renderer/read.ts b/src/remote/activitypub/renderer/read.ts deleted file mode 100644 index 95357f64d3..0000000000 --- a/src/remote/activitypub/renderer/read.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; -import { MessagingMessage } from '@/models/entities/messaging-message'; - -export const renderReadActivity = (user: { id: User['id'] }, message: MessagingMessage) => ({ - type: 'Read', - actor: `${config.url}/users/${user.id}`, - object: message.uri -}); diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts deleted file mode 100644 index 42beffecf2..0000000000 --- a/src/remote/activitypub/renderer/reject.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; - -export default (object: any, user: { id: User['id'] }) => ({ - type: 'Reject', - actor: `${config.url}/users/${user.id}`, - object -}); diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts deleted file mode 100644 index 79d60edbaa..0000000000 --- a/src/remote/activitypub/renderer/remove.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; - -export default (user: { id: User['id'] }, target: any, object: any) => ({ - type: 'Remove', - actor: `${config.url}/users/${user.id}`, - target, - object -}); diff --git a/src/remote/activitypub/renderer/tombstone.ts b/src/remote/activitypub/renderer/tombstone.ts deleted file mode 100644 index 553406b93b..0000000000 --- a/src/remote/activitypub/renderer/tombstone.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default (id: string) => ({ - id, - type: 'Tombstone' -}); diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts deleted file mode 100644 index 14115b788d..0000000000 --- a/src/remote/activitypub/renderer/undo.ts +++ /dev/null @@ -1,13 +0,0 @@ -import config from '@/config/index'; -import { ILocalUser, User } from '@/models/entities/user'; - -export default (object: any, user: { id: User['id'] }) => { - if (object == null) return null; - - return { - type: 'Undo', - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), - }; -}; diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts deleted file mode 100644 index 8bb415d117..0000000000 --- a/src/remote/activitypub/renderer/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index'; -import { User } from '@/models/entities/user'; - -export default (object: any, user: { id: User['id'] }) => { - const activity = { - id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user.id}`, - type: 'Update', - to: [ 'https://www.w3.org/ns/activitystreams#Public' ], - object, - published: new Date().toISOString(), - } as any; - - return activity; -}; diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts deleted file mode 100644 index ff038070f7..0000000000 --- a/src/remote/activitypub/renderer/vote.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index'; -import { Note } from '@/models/entities/note'; -import { IRemoteUser, User } from '@/models/entities/user'; -import { PollVote } from '@/models/entities/poll-vote'; -import { Poll } from '@/models/entities/poll'; - -export default async function renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> { - return { - id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`, - actor: `${config.url}/users/${user.id}`, - type: 'Create', - to: [pollOwner.uri], - published: new Date().toISOString(), - object: { - id: `${config.url}/users/${user.id}#votes/${vote.id}`, - type: 'Note', - attributedTo: `${config.url}/users/${user.id}`, - to: [pollOwner.uri], - inReplyTo: note.uri, - name: poll.choices[vote.choice] - } - }; -} diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts deleted file mode 100644 index d6ced630c1..0000000000 --- a/src/remote/activitypub/request.ts +++ /dev/null @@ -1,58 +0,0 @@ -import config from '@/config/index'; -import { getUserKeypair } from '@/misc/keypair-store'; -import { User } from '@/models/entities/user'; -import { getResponse } from '../../misc/fetch'; -import { createSignedPost, createSignedGet } from './ap-request'; - -export default async (user: { id: User['id'] }, url: string, object: any) => { - const body = JSON.stringify(object); - - const keypair = await getUserKeypair(user.id); - - const req = createSignedPost({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key` - }, - url, - body, - additionalHeaders: { - 'User-Agent': config.userAgent, - } - }); - - await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - body, - }); -}; - -/** - * Get AP object with http-signature - * @param user http-signature user - * @param url URL to fetch - */ -export async function signedGet(url: string, user: { id: User['id'] }) { - const keypair = await getUserKeypair(user.id); - - const req = createSignedGet({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key` - }, - url, - additionalHeaders: { - 'User-Agent': config.userAgent, - } - }); - - const res = await getResponse({ - url, - method: req.request.method, - headers: req.request.headers - }); - - return await res.json(); -} diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts deleted file mode 100644 index f392a65e3a..0000000000 --- a/src/remote/activitypub/resolver.ts +++ /dev/null @@ -1,73 +0,0 @@ -import config from '@/config/index'; -import { getJson } from '@/misc/fetch'; -import { ILocalUser } from '@/models/entities/user'; -import { getInstanceActor } from '@/services/instance-actor'; -import { signedGet } from './request'; -import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type'; -import { fetchMeta } from '@/misc/fetch-meta'; -import { extractDbHost } from '@/misc/convert-host'; - -export default class Resolver { - private history: Set<string>; - private user?: ILocalUser; - - constructor() { - this.history = new Set(); - } - - public getHistory(): string[] { - return Array.from(this.history); - } - - public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> { - const collection = typeof value === 'string' - ? await this.resolve(value) - : value; - - if (isCollectionOrOrderedCollection(collection)) { - return collection; - } else { - throw new Error(`unrecognized collection type: ${collection.type}`); - } - } - - public async resolve(value: string | IObject): Promise<IObject> { - if (value == null) { - throw new Error('resolvee is null (or undefined)'); - } - - if (typeof value !== 'string') { - return value; - } - - if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); - } - - this.history.add(value); - - const meta = await fetchMeta(); - const host = extractDbHost(value); - if (meta.blockedHosts.includes(host)) { - throw new Error('Instance is blocked'); - } - - if (config.signToActivityPubGet && !this.user) { - this.user = await getInstanceActor(); - } - - const object = this.user - ? await signedGet(value, this.user) - : await getJson(value, 'application/activity+json, application/ld+json'); - - if (object == null || ( - Array.isArray(object['@context']) ? - !object['@context'].includes('https://www.w3.org/ns/activitystreams') : - object['@context'] !== 'https://www.w3.org/ns/activitystreams' - )) { - throw new Error('invalid response'); - } - - return object; - } -} diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts deleted file mode 100644 index 2051d2624d..0000000000 --- a/src/remote/activitypub/type.ts +++ /dev/null @@ -1,289 +0,0 @@ -export type obj = { [x: string]: any }; -export type ApObject = IObject | string | (IObject | string)[]; - -export interface IObject { - '@context': string | obj | obj[]; - type: string | string[]; - id?: string; - summary?: string; - published?: string; - cc?: ApObject; - to?: ApObject; - attributedTo: ApObject; - attachment?: any[]; - inReplyTo?: any; - replies?: ICollection; - content?: string; - name?: string; - startTime?: Date; - endTime?: Date; - icon?: any; - image?: any; - url?: ApObject; - href?: string; - tag?: IObject | IObject[]; - sensitive?: boolean; -} - -/** - * Get array of ActivityStreams Objects id - */ -export function getApIds(value: ApObject | undefined): string[] { - if (value == null) return []; - const array = Array.isArray(value) ? value : [value]; - return array.map(x => getApId(x)); -} - -/** - * Get first ActivityStreams Object id - */ -export function getOneApId(value: ApObject): string { - const firstOne = Array.isArray(value) ? value[0] : value; - return getApId(firstOne); -} - -/** - * Get ActivityStreams Object id - */ -export function getApId(value: string | IObject): string { - if (typeof value === 'string') return value; - if (typeof value.id === 'string') return value.id; - throw new Error(`cannot detemine id`); -} - -/** - * Get ActivityStreams Object type - */ -export function getApType(value: IObject): string { - if (typeof value.type === 'string') return value.type; - if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - throw new Error(`cannot detect type`); -} - -export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { - const firstOne = Array.isArray(value) ? value[0] : value; - return getApHrefNullable(firstOne); -} - -export function getApHrefNullable(value: string | IObject | undefined): string | undefined { - if (typeof value === 'string') return value; - if (typeof value?.href === 'string') return value.href; - return undefined; -} - -export interface IActivity extends IObject { - //type: 'Activity'; - actor: IObject | string; - object: IObject | string; - target?: IObject | string; - /** LD-Signature */ - signature?: { - type: string; - created: Date; - creator: string; - domain?: string; - nonce?: string; - signatureValue: string; - }; -} - -export interface ICollection extends IObject { - type: 'Collection'; - totalItems: number; - items: ApObject; -} - -export interface IOrderedCollection extends IObject { - type: 'OrderedCollection'; - totalItems: number; - orderedItems: ApObject; -} - -export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; - -export const isPost = (object: IObject): object is IPost => - validPost.includes(getApType(object)); - -export interface IPost extends IObject { - type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; - _misskey_content?: string; - _misskey_quote?: string; - quoteUrl?: string; - _misskey_talk: boolean; -} - -export interface IQuestion extends IObject { - type: 'Note' | 'Question'; - _misskey_content?: string; - _misskey_quote?: string; - quoteUrl?: string; - oneOf?: IQuestionChoice[]; - anyOf?: IQuestionChoice[]; - endTime?: Date; - closed?: Date; -} - -export const isQuestion = (object: IObject): object is IQuestion => - getApType(object) === 'Note' || getApType(object) === 'Question'; - -interface IQuestionChoice { - name?: string; - replies?: ICollection; - _misskey_votes?: number; -} -export interface ITombstone extends IObject { - type: 'Tombstone'; - formerType?: string; - deleted?: Date; -} - -export const isTombstone = (object: IObject): object is ITombstone => - getApType(object) === 'Tombstone'; - -export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; - -export const isActor = (object: IObject): object is IActor => - validActor.includes(getApType(object)); - -export interface IActor extends IObject { - type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; - name?: string; - preferredUsername?: string; - manuallyApprovesFollowers?: boolean; - discoverable?: boolean; - inbox: string; - sharedInbox?: string; // 後方互換性のため - publicKey?: { - id: string; - publicKeyPem: string; - }; - followers?: string | ICollection | IOrderedCollection; - following?: string | ICollection | IOrderedCollection; - featured?: string | IOrderedCollection; - outbox: string | IOrderedCollection; - endpoints?: { - sharedInbox?: string; - }; - 'vcard:bday'?: string; - 'vcard:Address'?: string; -} - -export const isCollection = (object: IObject): object is ICollection => - getApType(object) === 'Collection'; - -export const isOrderedCollection = (object: IObject): object is IOrderedCollection => - getApType(object) === 'OrderedCollection'; - -export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => - isCollection(object) || isOrderedCollection(object); - -export interface IApPropertyValue extends IObject { - type: 'PropertyValue'; - identifier: IApPropertyValue; - name: string; - value: string; -} - -export const isPropertyValue = (object: IObject): object is IApPropertyValue => - object && - getApType(object) === 'PropertyValue' && - typeof object.name === 'string' && - typeof (object as any).value === 'string'; - -export interface IApMention extends IObject { - type: 'Mention'; - href: string; -} - -export const isMention = (object: IObject): object is IApMention=> - getApType(object) === 'Mention' && - typeof object.href === 'string'; - -export interface IApHashtag extends IObject { - type: 'Hashtag'; - name: string; -} - -export const isHashtag = (object: IObject): object is IApHashtag => - getApType(object) === 'Hashtag' && - typeof object.name === 'string'; - -export interface IApEmoji extends IObject { - type: 'Emoji'; - updated: Date; -} - -export const isEmoji = (object: IObject): object is IApEmoji => - getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; - -export interface ICreate extends IActivity { - type: 'Create'; -} - -export interface IDelete extends IActivity { - type: 'Delete'; -} - -export interface IUpdate extends IActivity { - type: 'Update'; -} - -export interface IRead extends IActivity { - type: 'Read'; -} - -export interface IUndo extends IActivity { - type: 'Undo'; -} - -export interface IFollow extends IActivity { - type: 'Follow'; -} - -export interface IAccept extends IActivity { - type: 'Accept'; -} - -export interface IReject extends IActivity { - type: 'Reject'; -} - -export interface IAdd extends IActivity { - type: 'Add'; -} - -export interface IRemove extends IActivity { - type: 'Remove'; -} - -export interface ILike extends IActivity { - type: 'Like' | 'EmojiReaction' | 'EmojiReact'; - _misskey_reaction?: string; -} - -export interface IAnnounce extends IActivity { - type: 'Announce'; -} - -export interface IBlock extends IActivity { - type: 'Block'; -} - -export interface IFlag extends IActivity { - type: 'Flag'; -} - -export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; -export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; -export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; -export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; -export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; -export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; -export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; -export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; -export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; -export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; -export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; -export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; -export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; -export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/src/remote/logger.ts b/src/remote/logger.ts deleted file mode 100644 index 9ffad4d716..0000000000 --- a/src/remote/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from '@/services/logger'; - -export const remoteLogger = new Logger('remote', 'cyan'); diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts deleted file mode 100644 index a12396abc8..0000000000 --- a/src/remote/resolve-user.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { URL } from 'url'; -import webFinger from './webfinger'; -import config from '@/config/index'; -import { createPerson, updatePerson } from './activitypub/models/person'; -import { remoteLogger } from './logger'; -import * as chalk from 'chalk'; -import { User, IRemoteUser } from '@/models/entities/user'; -import { Users } from '@/models/index'; -import { toPuny } from '@/misc/convert-host'; - -const logger = remoteLogger.createSubLogger('resolve-user'); - -export async function resolveUser(username: string, host: string | null, option?: any, resync = false): Promise<User> { - const usernameLower = username.toLowerCase(); - - if (host == null) { - logger.info(`return local user: ${usernameLower}`); - return await Users.findOne({ usernameLower, host: null }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - host = toPuny(host); - - if (config.host == host) { - logger.info(`return local user: ${usernameLower}`); - return await Users.findOne({ usernameLower, host: null }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser; - - const acctLower = `${usernameLower}@${host}`; - - if (user == null) { - const self = await resolveSelf(acctLower); - - logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); - return await createPerson(self.href); - } - - // resyncオプション OR ユーザー情報が古い場合は、WebFilgerからやりなおして返す - if (resync || user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await Users.update(user.id, { - lastFetchedAt: new Date(), - }); - - logger.info(`try resync: ${acctLower}`); - const self = await resolveSelf(acctLower); - - if (user.uri !== self.href) { - // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping. - logger.info(`uri missmatch: ${acctLower}`); - logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); - - // validate uri - const uri = new URL(self.href); - if (uri.hostname !== host) { - throw new Error(`Invalid uri`); - } - - await Users.update({ - usernameLower, - host: host - }, { - uri: self.href - }); - } else { - logger.info(`uri is fine: ${acctLower}`); - } - - await updatePerson(self.href); - - logger.info(`return resynced remote user: ${acctLower}`); - return await Users.findOne({ uri: self.href }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - logger.info(`return existing remote user: ${acctLower}`); - return user; -} - -async function resolveSelf(acctLower: string) { - logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); - const finger = await webFinger(acctLower).catch(e => { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`); - throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`); - }); - const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); - if (!self) { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); - throw new Error('self link not found'); - } - return self; -} diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts deleted file mode 100644 index f63fd03628..0000000000 --- a/src/remote/webfinger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { URL } from 'url'; -import { getJson } from '@/misc/fetch'; -import { query as urlQuery } from '@/prelude/url'; - -type ILink = { - href: string; - rel?: string; -}; - -type IWebFinger = { - links: ILink[]; - subject: string; -}; - -export default async function(query: string): Promise<IWebFinger> { - const url = genUrl(query); - - return await getJson(url, 'application/jrd+json, application/json'); -} - -function genUrl(query: string) { - if (query.match(/^https?:\/\//)) { - const u = new URL(query); - return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); - } - - const m = query.match(/^([^@]+)@(.*)/); - if (m) { - const hostname = m[2]; - return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` }); - } - - throw new Error(`Invalid query (${query})`); -} |