diff --git a/locales/en-US.yml b/locales/en-US.yml index 30e859b919..dd4b2fa008 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -878,6 +878,7 @@ createdAt: "Created at" invitationRequiredToRegister: "This server is currently invitation only. Only those with an invitation code can register." themeColor: "Theme Color" preferTickerSoftwareColor: "Prefer Software Color on instance ticker" +typeToConfirm: "Please enter {x} to confirm" _template: edit: "Edit Template..." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index eca2490e92..9811698fdb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -918,6 +918,7 @@ leaveGroupConfirm: "「{name}」から抜けますか?" notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" themeColor: "テーマカラー" preferTickerSoftwareColor: "インスタンスティッカーにソフトウェアカラーを採用して表示する" +typeToConfirm: "この操作を行うには {x} と入力してください" _template: edit: "定型文を編集…" diff --git a/package.json b/package.json index 7fe3742e1e..517b7b2f37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "groundpolis-milkey", - "version": "2022.07.03-milkey-2.9", + "version": "2022.07.03-milkey-3.0", "private": true, "author": "Xeltica , Minemu , Azuki⪥ , Remito , atsu1125 ", "contributors": [ diff --git a/src/client/pages/instance/user-dialog.vue b/src/client/pages/instance/user-dialog.vue index b941e878de..63ef2ed594 100644 --- a/src/client/pages/instance/user-dialog.vue +++ b/src/client/pages/instance/user-dialog.vue @@ -62,6 +62,7 @@ import XModalWindow from '@/components/ui/modal-window.vue'; import Progress from '@/scripts/loading'; import { acct, userPage } from '../../filters/user'; import * as os from '@/os'; +import { i18n } from '@/i18n'; export default defineComponent({ components: { @@ -223,16 +224,30 @@ export default defineComponent({ text: this.$ts.deleteAccountConfirm, }); if (confirm.canceled) return; - const process = async () => { - await os.api('admin/delete-account', { userId: this.user.id }); - os.success(); - }; - await process().catch(e => { + const { canceled, result: username } = await os.dialog({ + title: i18n.t('typeToConfirm', { x: this.user?.username }), + input: { + allowEmpty: false + } + }); + if (canceled) return; + if (username === this.user?.username) { + const process = async () => { + await os.api('admin/delete-account', { userId: this.user.id }); + os.success(); + }; + await process().catch(e => { + os.dialog({ + type: 'error', + text: e.toString() + }); + }); + } else { os.dialog({ type: 'error', - text: e.toString() + text: 'input not match', }); - }); + } await this.refreshUser(); }, diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue index 6d9983b959..c136ff0e2e 100644 --- a/src/client/pages/mfm-cheat-sheet.vue +++ b/src/client/pages/mfm-cheat-sheet.vue @@ -33,7 +33,7 @@ export default defineComponent({ [ 'hashtag', '#test' ], [ 'url', `https://example.com` ], [ 'link', `[${this.$ts._mfm.dummy}](https://example.com)` ], - [ 'emoji', `:${this.$instance.emojis[0].name}:` ], + [ 'emoji', this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:` ], [ 'userEmoji', `:@${this.$i.username}:` ], [ 'bold', `**${this.$ts._mfm.dummy}**` ], [ 'small', `${this.$ts._mfm.dummy}` ], diff --git a/src/misc/convert-host.ts b/src/misc/convert-host.ts index ad52e12588..1372d4a0e0 100644 --- a/src/misc/convert-host.ts +++ b/src/misc/convert-host.ts @@ -10,6 +10,16 @@ export function isSelfHost(host: string) { return toPuny(config.host) === toPuny(host); } +export function isSelfOrigin(src: unknown) { + if (typeof src !== 'string') return null; + try { + const u = new URL(src); + return u.origin === config.url; + } catch { + return false; + } +} + export function extractDbHost(uri: string) { const url = new URL(uri); return toPuny(url.hostname); diff --git a/src/misc/download-url.ts b/src/misc/download-url.ts index 37a60eee51..07d02bd307 100644 --- a/src/misc/download-url.ts +++ b/src/misc/download-url.ts @@ -8,10 +8,15 @@ import * as chalk from 'chalk'; import Logger from '../services/logger'; import * as IPCIDR from 'ip-cidr'; const PrivateIp = require('private-ip'); +import { isValidUrl } from './is-valid-url'; const pipeline = util.promisify(stream.pipeline); export async function downloadUrl(url: string, path: string) { + if (!isValidUrl(url)) { + throw new StatusError('Invalid URL', 400); + } + const logger = new Logger('download'); logger.info(`Downloading ${chalk.cyan(url)} ...`); @@ -20,29 +25,39 @@ export async function downloadUrl(url: string, path: string) { const operationTimeout = 60 * 1000; const maxSize = config.maxFileSize || 262144000; - const req = got.stream(url, { - headers: { - 'User-Agent': config.userAgent - }, - timeout: { - lookup: timeout, - connect: timeout, - secureConnect: timeout, - socket: timeout, // read timeout - response: timeout, - send: timeout, - request: operationTimeout, // whole operation timeout - }, - agent: { - http: httpAgent, - https: httpsAgent, - }, - http2: false, // default - retry: 0, - }).on('response', (res: Got.Response) => { - if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) { - if (isPrivateIp(res.ip)) { - logger.warn(`Blocked address: ${res.ip}`); + const req = got + .stream(url, { + headers: { + 'User-Agent': config.userAgent, + }, + timeout: { + lookup: timeout, + connect: timeout, + secureConnect: timeout, + socket: timeout, // read timeout + response: timeout, + send: timeout, + request: operationTimeout, // whole operation timeout + }, + agent: { + http: httpAgent, + https: httpsAgent, + }, + http2: false, // default + retry: { + limit: 0, + }, + }) + .on('redirect', (res: Got.Response, opts: Got.NormalizedOptions) => { + if (!isValidUrl(opts.url)) { + logger.warn(`Invalid URL: ${opts.url}`); + req.destroy(); + } + }) + .on('response', (res: Got.Response) => { + if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) { + if (isPrivateIp(res.ip)) { + logger.warn(`Blocked address: ${res.ip}`); req.destroy(); } } diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts index eb4ec17308..eeff218cc3 100644 --- a/src/misc/fetch.ts +++ b/src/misc/fetch.ts @@ -1,10 +1,11 @@ import * as http from 'http'; import * as https from 'https'; import CacheableLookup from 'cacheable-lookup'; -import fetch from 'node-fetch'; +import fetch, { RequestRedirect } from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import config from '../config'; import { URL } from 'url'; +import { isValidUrl } from './is-valid-url'; export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record) { const res = await getResponse({ @@ -34,8 +35,20 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout = return await res.text(); } -export async function getResponse(args: { url: string, method: string, body?: string, headers: Record, timeout?: number, size?: number }) { - const timeout = args?.timeout || 10 * 1000; +export async function getResponse(args: { + url: string; + method: string; + body?: string; + headers: Record; + timeout?: number; + size?: number; + redirect?: RequestRedirect; +}) { + if (!isValidUrl(args.url)) { + throw new StatusError('Invalid URL', 400); + } + + const timeout = args.timeout || 10 * 1000; const controller = new AbortController(); setTimeout(() => { @@ -50,8 +63,16 @@ export async function getResponse(args: { url: string, method: string, body?: st size: args?.size || 10 * 1024 * 1024, agent: getAgentByUrl, signal: controller.signal, + redirect: args.redirect, }); + if (args.redirect === 'manual' && [301, 302, 307, 308].includes(res.status)) { + if (!isValidUrl(res.url)) { + throw new StatusError('Invalid URL', 400); + } + return res; + } + if (!res.ok) { throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); } diff --git a/src/misc/is-valid-url.ts b/src/misc/is-valid-url.ts new file mode 100644 index 0000000000..485d14a298 --- /dev/null +++ b/src/misc/is-valid-url.ts @@ -0,0 +1,20 @@ +export function isValidUrl(url: string | URL | undefined): boolean { + if (process.env.NODE_ENV !== 'production') return true; + + try { + if (url == null) return false; + + const u = typeof url === 'string' ? new URL(url) : url; + if (!u.protocol.match(/^https?:$/) || u.hostname === 'unix') { + return false; + } + + if (u.port !== '' && !['80', '443'].includes(u.port)) { + return false; + } + + return true; + } catch { + return false; + } +} diff --git a/src/remote/activitypub/db-resolver.ts b/src/remote/activitypub/db-resolver.ts index 8b436be37c..716e06bef4 100644 --- a/src/remote/activitypub/db-resolver.ts +++ b/src/remote/activitypub/db-resolver.ts @@ -9,6 +9,49 @@ import { resolvePerson } from './models/person'; import { ensure } from '../../prelude/ensure'; import escapeRegexp = require('escape-regexp'); +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 type AuthUser = { + user: IRemoteUser; + key?: UserPublickey; +}; + +export function parseUri(value: string) : 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() { } @@ -17,60 +60,54 @@ export default class DbResolver { * AP Note => Groundpolis Note in DB */ public async getNoteFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); - if (parsed.id) { - return (await Notes.findOne({ - id: parsed.id - })) || null; - } + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.uri) { - return (await Notes.findOne({ - uri: parsed.uri - })) || null; + return await Notes.findOne({ + id: parsed.id, + }); + } else { + return await Notes.findOne({ + uri: parsed.uri, + }); } - - return null; } public async getMessageFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); - if (parsed.id) { - return (await MessagingMessages.findOne({ - id: parsed.id - })) || null; - } + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.uri) { - return (await MessagingMessages.findOne({ - uri: parsed.uri - })) || null; + return await MessagingMessages.findOne({ + id: parsed.id, + }); + } else { + return await MessagingMessages.findOne({ + uri: parsed.uri, + }); } - - return null; } /** * AP Person => Groundpolis User in DB */ public async getUserFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'users') return null; - if (parsed.id) { return (await Users.findOne({ id: parsed.id })) || null; - } - - if (parsed.uri) { + } else { return (await Users.findOne({ uri: parsed.uri })) || null; } - - return null; } /** @@ -106,36 +143,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 - }; - } - } } - -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/models/note.ts b/src/remote/activitypub/models/note.ts index 4e248f4ae7..0d72b8f1fb 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -14,7 +14,7 @@ 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 { extractDbHost, toPuny, isSelfOrigin } from '../../../misc/convert-host'; import { Emojis, Polls, MessagingMessages } from '../../../models'; import { Note } from '../../../models/entities/note'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type'; @@ -142,7 +142,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s }).catch(async e => { // トークだったらinReplyToのエラーは無視 const uri = getApId(note.inReplyTo); - if (uri.startsWith(config.url + '/')) { + if (isSelfOrigin(uri)) { const id = uri.split('/').pop(); const talk = await MessagingMessages.findOne(id); if (talk) { @@ -307,7 +307,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): } //#endregion - if (uri.startsWith(config.url)) { + if (isSelfOrigin(uri)) { throw { statusCode: 400 }; } diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 873bdc91e1..ce85106c61 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -20,7 +20,7 @@ import { genId } from '../../../misc/gen-id'; import { instanceChart, usersChart } from '../../../services/chart'; import { UserPublickey } from '../../../models/entities/user-publickey'; import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; -import { toPuny } from '../../../misc/convert-host'; +import { toPuny, isSelfOrigin } from '../../../misc/convert-host'; import { UserProfile } from '../../../models/entities/user-profile'; import { validActor } from '../../../remote/activitypub/type'; import { getConnection } from 'typeorm'; @@ -97,7 +97,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise x || null); } @@ -119,7 +119,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); - if (uri.startsWith(config.url)) { + if (isSelfOrigin(uri)) { throw { statusCode: 400 }; } @@ -288,7 +288,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) { + if (isSelfOrigin(uri)) { return; } diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index 8199b45c16..674ffb83bd 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -4,6 +4,7 @@ import { IObject, IQuestion, isQuestion, } from '../type'; import { apLogger } from '../logger'; import { Notes, Polls } from '../../../models'; import { IPoll } from '../../../models/entities/poll'; +import { isSelfOrigin } from '../../../misc/convert-host'; export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { if (resolver == null) resolver = new Resolver(); @@ -44,7 +45,7 @@ export async function updateQuestion(value: any, resolver?: Resolver) { const uri = typeof value === 'string' ? value : value.id; // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); + if (isSelfOrigin(uri)) throw new Error('uri points local'); //#region このサーバーに既に登録されているか const note = await Notes.findOne({ uri }); diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index dd93b4cd32..905cacdb83 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -6,6 +6,8 @@ import { getResponse } from '../../misc/fetch'; import { createSignedPost, createSignedGet } from './ap-request'; import type { Response } from 'node-fetch'; import { IObject } from './type'; +import { isValidUrl } from '../../misc/is-valid-url'; +import { apLogger } from './logger'; export default async (user: ILocalUser, url: string, object: any) => { const body = JSON.stringify(object); @@ -36,8 +38,9 @@ export default async (user: ILocalUser, url: string, object: any) => { /** * Get ActivityPub object - * @param user http-signature user * @param url URL to fetch + * @param user http-signature user + * @param redirects whether or not to accept redirects */ export async function signedGet(url: string, user: ILocalUser) { const keypair = await UserKeypairs.findOne({ @@ -64,7 +67,15 @@ export async function signedGet(url: string, user: ILocalUser) { return await res.json(); } -export async function apGet(url: string, user?: ILocalUser): Promise { +export async function apGet( + url: string, + user?: ILocalUser, + redirects: boolean = true, +): Promise<{ finalUrl: string; content: IObject }> { + if (!isValidUrl(url)) { + throw new StatusError('Invalid URL', 400); + } + let res: Response; if (user != null) { @@ -86,7 +97,15 @@ export async function apGet(url: string, user?: ILocalUser): Promise { url, method: req.request.method, headers: req.request.headers, + redirect: redirects ? 'manual' : 'error', }); + + if (redirects && [301, 302, 307, 308].includes(res.status)) { + const newUrl = res.headers.get('location'); + if (newUrl == null) throw new Error('apGet got redirect but no target location'); + apLogger.debug(`apGet is redirecting to ${newUrl}`); + return apGet(newUrl, user, false); + } } else { res = await getResponse({ url, @@ -96,12 +115,20 @@ export async function apGet(url: string, user?: ILocalUser): Promise { 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', "User-Agent": config.userAgent, }, + redirect: redirects ? 'manual' : 'error', }); + + if (redirects && [301, 302, 307, 308].includes(res.status)) { + const newUrl = res.headers.get('location'); + if (newUrl == null) throw new Error('apGet got redirect but no target location'); + apLogger.debug(`apGet is redirecting to ${newUrl}`); + return apGet(newUrl, undefined, false); + } } const contentType = res.headers.get("content-type"); if (contentType == null || !validateContentType(contentType)) { - throw new Error("Invalid Content Type"); + throw new Error(`apGet response had unexpected content-type: ${contentType}`); } if (res.body == null) throw new Error("body is null"); @@ -109,7 +136,10 @@ export async function apGet(url: string, user?: ILocalUser): Promise { const text = await res.text(); if (text.length > 65536) throw new Error("too big result"); - return JSON.parse(text) as IObject; + return { + finalUrl: res.url, + content: JSON.parse(text) as IObject, + }; } function validateContentType(contentType: string): boolean { diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 0a3d0e1663..f9c0f03995 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,11 +1,20 @@ import config from '../../config'; -import { getJson } from '../../misc/fetch'; import { ILocalUser } from '../../models/entities/user'; import { getInstanceActor } from '../../services/instance-actor'; import { apGet } from './request'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type'; +import { FollowRequests, Notes, NoteReactions, Polls, Users } from '../../models'; +import { parseUri } from './db-resolver'; import { fetchMeta } from '../../misc/fetch-meta'; -import { extractDbHost } from '../../misc/convert-host'; +import { extractDbHost, isSelfHost } from '../../misc/convert-host'; +import renderNote from './renderer/note'; +import { renderLike } from './renderer/like'; +import { renderPerson } from './renderer/person'; +import renderQuestion from './renderer/question'; +import renderCreate from './renderer/create'; +import { renderActivity } from './renderer/index'; +import renderFollow from './renderer/follow'; +import { IsNull, Not } from 'typeorm'; export default class Resolver { private history: Set; @@ -42,6 +51,13 @@ 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'); } @@ -52,8 +68,12 @@ export default class Resolver { 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'); } @@ -62,7 +82,7 @@ export default class Resolver { this.user = await getInstanceActor(); } - const object = await apGet(value, this.user); + const { finalUrl, content: object } = await apGet(value, this.user); if (object == null || ( Array.isArray(object['@context']) ? @@ -72,6 +92,81 @@ export default class Resolver { throw new Error('invalid response'); } - return object; + if (object.id == null) { + throw new Error('Object has no ID'); + } + + if (finalUrl === object.id) return object; + + if (new URL(finalUrl).host !== new URL(object.id).host) { + throw new Error("Object ID host doesn't match final url host"); + } + + const finalRes = await apGet(object.id, this.user); + + if (finalRes.finalUrl !== finalRes.content.id) + throw new Error( + "Object ID still doesn't match final URL after second fetch attempt", + ); + + return finalRes.content; + } + + private async resolveLocal(url: string): Promise { + const parsed = parseUri(url); + if (!parsed.local) throw new Error('resolveLocal: not local'); + + switch (parsed.type) { + case 'notes': + const note = await Notes.findOneOrFail({ id: parsed.id }); + if (parsed.rest === 'activity') { + // this refers to the create activity and not the note itself + return renderActivity(renderCreate(renderNote(note), note)); + } else { + return renderNote(note); + } + case 'users': + const user = await Users.findOneOrFail({ id: parsed.id }); + return await renderPerson(user as ILocalUser); + case 'questions': + // Polls are indexed by the note they are attached to. + const [pollNote, poll] = await Promise.all([ + Notes.findOneOrFail({ id: parsed.id }), + Polls.findOneOrFail({ noteId: parsed.id }), + ]); + return await renderQuestion({ id: pollNote.userId }, pollNote, poll); + case 'likes': + const reaction = await NoteReactions.findOneOrFail({ id: parsed.id }); + return renderActivity(renderLike(reaction, { uri: null })); + case 'follows': + // if rest is a + if (parsed.rest != null && /^\w+$/.test(parsed.rest)) { + const [follower, followee] = await Promise.all( + [parsed.id, parsed.rest].map((id) => Users.findOneOrFail({ id }))); + return renderActivity(renderFollow(follower, followee, url)); + } + + // Another situation is there is only requestId, then obtained object from database. + const followRequest = await FollowRequests.findOne({ + id: parsed.id, + }); + if (followRequest == null) { + throw new Error('resolveLocal: invalid follow URI'); + } + const follower = await Users.findOne({ + id: followRequest.followerId, + host: IsNull(), + }); + const followee = await Users.findOne({ + id: followRequest.followeeId, + host: Not(IsNull()), + }); + if (follower == null || followee == null) { + throw new Error('resolveLocal: invalid follow URI'); + } + return renderActivity(renderFollow(follower, followee, url)); + default: + throw new Error(`resolveLocal: type ${parsed.type} unhandled`); + } } } diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 756116f922..c0c0e28618 100644 --- a/src/server/api/endpoints/ap/show.ts +++ b/src/server/api/endpoints/ap/show.ts @@ -5,7 +5,7 @@ import { createPerson } from '../../../../remote/activitypub/models/person'; import { createNote } from '../../../../remote/activitypub/models/note'; import Resolver from '../../../../remote/activitypub/resolver'; import { ApiError } from '../../error'; -import { extractDbHost } from '../../../../misc/convert-host'; +import { extractDbHost, isSelfOrigin } from '../../../../misc/convert-host'; import { Users, Notes } from '../../../../models'; import { Note } from '../../../../models/entities/note'; import { User } from '../../../../models/entities/user'; @@ -53,7 +53,7 @@ export default define(meta, async (ps) => { */ async function fetchAny(uri: string) { // URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ - if (uri.startsWith(config.url + '/')) { + if (isSelfOrigin(uri)) { const parts = uri.split('/'); const id = parts.pop(); const type = parts.pop();