summaryrefslogtreecommitdiff
path: root/packages/backend/src/remote
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-06-11 19:31:03 +0900
committerGitHub <noreply@github.com>2022-06-11 19:31:03 +0900
commit182a1bf653ecfbcf76e4530b3077c6252b0d4827 (patch)
tree45d1472747d4cac017e96616f844292f5785ccdd /packages/backend/src/remote
parent12.110.1 (diff)
parent12.111.0 (diff)
downloadmisskey-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')
-rw-r--r--packages/backend/src/remote/activitypub/db-resolver.ts105
-rw-r--r--packages/backend/src/remote/activitypub/kernel/announce/note.ts3
-rw-r--r--packages/backend/src/remote/activitypub/kernel/delete/index.ts22
-rw-r--r--packages/backend/src/remote/activitypub/kernel/move/index.ts0
-rw-r--r--packages/backend/src/remote/activitypub/kernel/undo/announce.ts1
-rw-r--r--packages/backend/src/remote/activitypub/misc/get-note-html.ts6
-rw-r--r--packages/backend/src/remote/activitypub/models/mention.ts8
-rw-r--r--packages/backend/src/remote/activitypub/models/note.ts15
-rw-r--r--packages/backend/src/remote/activitypub/models/person.ts62
-rw-r--r--packages/backend/src/remote/activitypub/renderer/block.ts24
-rw-r--r--packages/backend/src/remote/activitypub/renderer/flag.ts2
-rw-r--r--packages/backend/src/remote/activitypub/renderer/follow.ts3
-rw-r--r--packages/backend/src/remote/activitypub/renderer/index.ts2
-rw-r--r--packages/backend/src/remote/activitypub/renderer/note.ts26
-rw-r--r--packages/backend/src/remote/activitypub/resolver.ts72
-rw-r--r--packages/backend/src/remote/activitypub/type.ts16
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[];