diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-06-11 19:31:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-11 19:31:03 +0900 |
| commit | 182a1bf653ecfbcf76e4530b3077c6252b0d4827 (patch) | |
| tree | 45d1472747d4cac017e96616f844292f5785ccdd /packages/backend/src/remote | |
| parent | 12.110.1 (diff) | |
| parent | 12.111.0 (diff) | |
| download | misskey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.tar.gz misskey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.tar.bz2 misskey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.zip | |
Merge pull request #8783 from misskey-dev/develop
Release: 12.111.0
Diffstat (limited to 'packages/backend/src/remote')
16 files changed, 237 insertions, 130 deletions
diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index ef07966e42..1a02f675ca 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/ import { UserPublickey } from '@/models/entities/user-publickey.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; -import { IObject, getApId } from './type.js'; -import { resolvePerson } from './models/person.js'; import { Cache } from '@/misc/cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; +import { IObject, getApId } from './type.js'; +import { resolvePerson } from './models/person.js'; const publicKeyCache = new Cache<UserPublickey | null>(Infinity); const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); +export type UriParseResult = { + /** wether the URI was generated by us */ + local: true; + /** id in DB */ + id: string; + /** hint of type, e.g. "notes", "users" */ + type: string; + /** any remaining text after type and id, not including the slash after id. undefined if empty */ + rest?: string; +} | { + /** wether the URI was generated by us */ + local: false; + /** uri in DB */ + uri: string; +}; + +export function parseUri(value: string | IObject): UriParseResult { + const uri = getApId(value); + + // the host part of a URL is case insensitive, so use the 'i' flag. + const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + local: true, + type: matchLocal[1], + id: matchLocal[2], + rest: matchLocal[3], + }; + } else { + return { + local: false, + uri, + }; + } +} + export default class DbResolver { constructor() { } @@ -21,60 +59,54 @@ export default class DbResolver { * AP Note => Misskey Note in DB */ public async getNoteFromApId(value: string | IObject): Promise<Note | null> { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await Notes.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await Notes.findOneBy({ uri: parsed.uri, }); } - - return null; } public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await MessagingMessages.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await MessagingMessages.findOneBy({ uri: parsed.uri, }); } - - return null; } /** * AP Person => Misskey User in DB */ public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'users') return null; - if (parsed.id) { return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ id: parsed.id, }).then(x => x ?? undefined)) ?? null; - } - - if (parsed.uri) { + } else { return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ uri: parsed.uri, })); } - - return null; } /** @@ -120,31 +152,4 @@ export default class DbResolver { 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, - }; - } - } } - -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/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index 680749f4d8..759cb4ae83 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { getApLock } from '@/misc/app-lock.js'; import { parseAudience } from '../../audience.js'; import { StatusError } from '@/misc/fetch.js'; +import { Notes } from '@/models/index.js'; const logger = apLogger; @@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac throw e; } + if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity'; + logger.info(`Creating the (Re)Note: ${uri}`); const activityAudience = await parseAudience(actor, activity.to, activity.cc); diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index 4c06a9de0b..c7064f553b 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st } // 削除対象objectのtype - let formarType: string | undefined; + let formerType: string | undefined; if (typeof activity.object === 'string') { // typeが不明だけど、どうせ消えてるのでremote resolveしない - formarType = undefined; + formerType = undefined; } else { const object = activity.object as IObject; if (isTombstone(object)) { - formarType = toSingle(object.formerType); + formerType = toSingle(object.formerType); } else { - formarType = toSingle(object.type); + formerType = toSingle(object.type); } } const uri = getApId(activity.object); // type不明でもactorとobjectが同じならばそれはPersonに違いない - if (!formarType && actor.uri === uri) { - formarType = 'Person'; + if (!formerType && actor.uri === uri) { + formerType = 'Person'; } // それでもなかったらおそらくNote - if (!formarType) { - formarType = 'Note'; + if (!formerType) { + formerType = 'Note'; } - if (validPost.includes(formarType)) { + if (validPost.includes(formerType)) { return await deleteNote(actor, uri); - } else if (validActor.includes(formarType)) { + } else if (validActor.includes(formerType)) { return await deleteActor(actor, uri); } else { - return `Unknown type ${formarType}`; + return `Unknown type ${formerType}`; } }; diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts deleted file mode 100644 index e69de29bb2..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/move/index.ts +++ /dev/null diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts index c2ac31bf8d..417f39722f 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts @@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun const note = await Notes.findOneBy({ uri, + userId: actor.id, }); if (!note) return 'skip: no such Announce'; diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts index 3800b40608..389039ebed 100644 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ b/packages/backend/src/remote/activitypub/misc/get-note-html.ts @@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js'; import { toHtml } from '../../../mfm/to-html.js'; export default function(note: Note) { - let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; - if (html == null) html = '<p>.</p>'; - - return html; + if (!note.text) return ''; + return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); } diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts index a160092969..13f77424ec 100644 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ b/packages/backend/src/remote/activitypub/models/mention.ts @@ -1,9 +1,9 @@ +import promiseLimit from 'promise-limit'; import { toArray, unique } from '@/prelude/array.js'; +import { CacheableUser, User } from '@/models/entities/user.js'; import { IObject, isMention, IApMention } from '../type.js'; -import { resolvePerson } from './person.js'; -import promiseLimit from 'promise-limit'; import Resolver from '../resolver.js'; -import { CacheableUser, User } from '@/models/entities/user.js'; +import { resolvePerson } from './person.js'; export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); @@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef const limit = promiseLimit<CacheableUser | null>(2); const mentionedUsers = (await Promise.all( - hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) + hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), )).filter((x): x is CacheableUser => x != null); return mentionedUsers; diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 097a716614..56c1a483ad 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit'; import config from '@/config/index.js'; import Resolver from '../resolver.js'; import post from '@/services/note/create.js'; -import { resolvePerson, updatePerson } from './person.js'; +import { resolvePerson } from './person.js'; import { resolveImage } from './image.js'; -import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { htmlToMfm } from '../misc/html-to-mfm.js'; import { extractApHashtags } from './tag.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; @@ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; +import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { Emoji } from '@/models/entities/emoji.js'; @@ -197,7 +197,14 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s const cw = note.summary === '' ? null : note.summary; // テキストのパース - const text = typeof note._misskey_content !== 'undefined' ? note._misskey_content : (note.content ? htmlToMfm(note.content, note.tag) : null); + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content === 'string') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = htmlToMfm(note.content, note.tag); + } // vote if (reply && reply.hasPoll) { diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 88661865da..6097e3b6ed 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -1,17 +1,8 @@ import { URL } from 'node:url'; import promiseLimit from 'promise-limit'; -import $, { Context } from 'cafy'; import config from '@/config/index.js'; -import Resolver from '../resolver.js'; -import { resolveImage } from './image.js'; -import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; -import { fromHtml } from '../../../mfm/from-html.js'; -import { htmlToMfm } from '../misc/html-to-mfm.js'; -import { resolveNote, extractEmojis } from './note.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { extractApHashtags } from './tag.js'; -import { apLogger } from '../logger.js'; import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; @@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js'; import { uriPersonCache } from '@/services/user-cache.js'; import { publishInternalEvent } from '@/services/stream.js'; import { db } from '@/db/postgre.js'; +import { apLogger } from '../logger.js'; +import { htmlToMfm } from '../misc/html-to-mfm.js'; +import { fromHtml } from '../../../mfm/from-html.js'; +import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; +import Resolver from '../resolver.js'; +import { extractApHashtags } from './tag.js'; +import { resolveNote, extractEmojis } from './note.js'; +import { resolveImage } from './image.js'; const logger = apLogger; @@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor { 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}`); - }; + if (!(typeof x.id === 'string' && x.id.length > 0)) { + throw new Error('invalid Actor: wrong id'); + } - validate('id', x.id, $.default.str.min(1)); - validate('inbox', x.inbox, $.default.str.min(1)); - validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/)); + if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { + throw new Error('invalid Actor: wrong inbox'); + } + + if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { + throw new Error('invalid Actor: wrong username'); + } // 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), $.default.optional.nullable.str); - validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str); + if (x.name) { + if (!(typeof x.name === 'string' && x.name.length > 0)) { + throw new Error('invalid Actor: wrong name'); + } + x.name = truncate(x.name, nameLength); + } + if (x.summary) { + if (!(typeof x.summary === 'string' && x.summary.length > 0)) { + throw new Error('invalid Actor: wrong summary'); + } + x.summary = truncate(x.summary, summaryLength); + } const idHost = toPuny(new URL(x.id!).hostname); if (idHost !== expectHost) { @@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ -export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: Record<string, unknown>): Promise<void> { +export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ @@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint if (resolver == null) resolver = new Resolver(); - const object = hint || await resolver.resolve(uri) as any; + const object = hint || await resolver.resolve(uri); const person = validateActor(object, uri); @@ -400,10 +412,10 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C 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), -}; + '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') { @@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) { // Resolve to (Ordered)Collection Object const collection = await resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`); + 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; diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts index 10a4fde517..13815fb76f 100644 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ b/packages/backend/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,20 @@ import config from '@/config/index.js'; -import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; +import { Blocking } from '@/models/entities/blocking.js'; -export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ - type: 'Block', - actor: `${config.url}/users/${blocker.id}`, - object: blockee.uri, -}); +/** + * Renders a block into its ActivityPub representation. + * + * @param block The block to be rendered. The blockee relation must be loaded. + */ +export function renderBlock(block: Blocking) { + if (block.blockee?.url == null) { + throw new Error('renderBlock: missing blockee uri'); + } + + return { + type: 'Block', + id: `${config.url}/blocks/${block.id}`, + actor: `${config.url}/users/${block.blockerId}`, + object: block.blockee.uri, + }; +} diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts index 6fbc11580f..58eadddbaa 100644 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ b/packages/backend/src/remote/activitypub/renderer/flag.ts @@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js'; // to anonymise reporters, the reporting actor must be a system user // object has to be a uri or array of uris -export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => { +export const renderFlag = (user: ILocalUser, object: [string], content: string) => { return { type: 'Flag', actor: `${config.url}/users/${user.id}`, diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts index 9e9692b77a..00fac18ad5 100644 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ b/packages/backend/src/remote/activitypub/renderer/follow.ts @@ -4,12 +4,11 @@ import { Users } from '@/models/index.js'; 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 = { + id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, 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/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index 5f69332266..f100b77ce5 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js'; export const renderActivity = (x: any): IActivity | null => { if (x == null) return null; - if (x !== null && typeof x === 'object' && x.id == null) { + if (typeof x === 'object' && x.id == null) { x.id = `${config.url}/${uuid()}`; } diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 679c8bbfe4..df2ae65205 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,15 +1,15 @@ -import renderDocument from './document.js'; -import renderHashtag from './hashtag.js'; -import renderMention from './mention.js'; -import renderEmoji from './emoji.js'; +import { In, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import toHtml from '../misc/get-note-html.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; -import { In, IsNull } from 'typeorm'; import { Emoji } from '@/models/entities/emoji.js'; import { Poll } from '@/models/entities/poll.js'; +import toHtml from '../misc/get-note-html.js'; +import renderEmoji from './emoji.js'; +import renderMention from './mention.js'; +import renderHashtag from './hashtag.js'; +import renderDocument from './document.js'; export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> { const getPromisedFiles = async (ids: string[]) => { @@ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const files = await getPromisedFiles(note.fileIds); - const text = note.text; - let poll: Poll | null; + // text should never be undefined + const text = note.text ?? null; + let poll: Poll | null = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); } - let apText = text; - if (apText == null) apText = ''; + let apText = text ?? ''; if (quote) { apText += `\n\nRE: ${quote}`; @@ -138,6 +138,10 @@ export default async function renderNote(note: Note, dive = true, isTalk = false summary, content, _misskey_content: text, + source: { + content: text, + mediaType: "text/x.misskeymarkdown", + }, _misskey_quote: quote, quoteUrl: quote, published: note.createdAt.toISOString(), @@ -159,7 +163,7 @@ export async function getEmojis(names: string[]): Promise<Emoji[]> { names.map(name => Emojis.findOneBy({ name, host: IsNull(), - })) + })), ); return emojis.filter(emoji => emoji != null) as Emoji[]; diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index c1269c75c5..2f9af43c0c 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -2,10 +2,19 @@ import config from '@/config/index.js'; import { getJson } from '@/misc/fetch.js'; import { ILocalUser } from '@/models/entities/user.js'; import { getInstanceActor } from '@/services/instance-actor.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; import { signedGet } from './request.js'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js'; +import { parseUri } from './db-resolver.js'; +import renderNote from '@/remote/activitypub/renderer/note.js'; +import { renderLike } from '@/remote/activitypub/renderer/like.js'; +import { renderPerson } from '@/remote/activitypub/renderer/person.js'; +import renderQuestion from '@/remote/activitypub/renderer/question.js'; +import renderCreate from '@/remote/activitypub/renderer/create.js'; +import { renderActivity } from '@/remote/activitypub/renderer/index.js'; +import renderFollow from '@/remote/activitypub/renderer/follow.js'; export default class Resolver { private history: Set<string>; @@ -40,14 +49,25 @@ export default class Resolver { return value; } + if (value.includes('#')) { + // URLs with fragment parts cannot be resolved correctly because + // the fragment part does not get transmitted over HTTP(S). + // Avoid strange behaviour by not trying to resolve these at all. + throw new Error(`cannot resolve URL with fragment: ${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 (isSelfHost(host)) { + return await this.resolveLocal(value); + } + + const meta = await fetchMeta(); if (meta.blockedHosts.includes(host)) { throw new Error('Instance is blocked'); } @@ -56,13 +76,13 @@ export default class Resolver { this.user = await getInstanceActor(); } - const object = this.user + const object = (this.user ? await signedGet(value, this.user) - : await getJson(value, 'application/activity+json, application/ld+json'); + : await getJson(value, 'application/activity+json, application/ld+json')) as IObject; if (object == null || ( Array.isArray(object['@context']) ? - !object['@context'].includes('https://www.w3.org/ns/activitystreams') : + !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { throw new Error('invalid response'); @@ -70,4 +90,44 @@ export default class Resolver { return object; } + + private resolveLocal(url: string): Promise<IObject> { + const parsed = parseUri(url); + if (!parsed.local) throw new Error('resolveLocal: not local'); + + switch (parsed.type) { + case 'notes': + return Notes.findOneByOrFail({ id: parsed.id }) + .then(note => { + if (parsed.rest === 'activity') { + // this refers to the create activity and not the note itself + return renderActivity(renderCreate(renderNote(note))); + } else { + return renderNote(note); + } + }); + case 'users': + return Users.findOneByOrFail({ id: parsed.id }) + .then(user => renderPerson(user as ILocalUser)); + case 'questions': + // Polls are indexed by the note they are attached to. + return Promise.all([ + Notes.findOneByOrFail({ id: parsed.id }), + Polls.findOneByOrFail({ noteId: parsed.id }), + ]) + .then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll)); + case 'likes': + return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null }))); + case 'follows': + // rest should be <followee id> + if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); + + return Promise.all( + [parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id })) + ) + .then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url))); + default: + throw new Error(`resolveLocal: type ${type} unhandled`); + } + } } diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index 2051d2624d..5d00481b75 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -2,7 +2,7 @@ export type obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { - '@context': string | obj | obj[]; + '@context': string | string[] | obj | obj[]; type: string | string[]; id?: string; summary?: string; @@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string { 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`); + throw new Error('cannot detemine id'); } /** @@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string { 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`); + throw new Error('cannot detect type'); } export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { @@ -106,7 +106,10 @@ export const isPost = (object: IObject): object is IPost => export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; _misskey_talk: boolean; @@ -114,7 +117,10 @@ export interface IPost extends IObject { export interface IQuestion extends IObject { type: 'Note' | 'Question'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; oneOf?: IQuestionChoice[]; |