import escapeRegexp from "escape-regexp"; import config from "@/config/index.js"; import type { Note } from "@/models/entities/note.js"; import type { CacheableRemoteUser, CacheableUser, } from "@/models/entities/user.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import { Notes, Users, UserPublickeys, MessagingMessages, } from "@/models/index.js"; import { Cache } from "@/misc/cache.js"; import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; import type { IObject } from "./type.js"; import { getApId } from "./type.js"; import { resolvePerson, updatePerson } from "./models/person.js"; import {redisClient, subscriber} from "@/db/redis.js"; const publicKeyCache = new Cache("publicKey", 60 * 30); const publicKeyByUserIdCache = new Cache( "publicKeyByUserId", 60 * 30, ); 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 { /** * AP Note => Misskey Note in DB */ public async getNoteFromApId(value: string | IObject): Promise { const parsed = parseUri(value); if (parsed.local) { if (parsed.type !== "notes") return null; return await Notes.findOneBy({ id: parsed.id, }); } else { return await Notes.findOne({ where: [ { uri: parsed.uri, }, { url: parsed.uri, }, ], }); } } public async getMessageFromApId( value: string | IObject, ): Promise { const parsed = parseUri(value); if (parsed.local) { if (parsed.type !== "notes") return null; return await MessagingMessages.findOneBy({ id: parsed.id, }); } else { return await MessagingMessages.findOneBy({ uri: parsed.uri, }); } } /** * AP Person => Misskey User in DB */ public async getUserFromApId( value: string | IObject, ): Promise { const parsed = parseUri(value); if (parsed.local) { if (parsed.type !== "users") return null; return ( (await userByIdCache.fetchMaybe( parsed.id, () => Users.findOneBy({ id: parsed.id, }).then((x) => x ?? undefined), true, )) ?? null ); } else { return await uriPersonCache.fetch( parsed.uri, () => Users.findOneBy({ uri: parsed.uri, }), true, ); } } /** * AP KeyId => Misskey User and Key */ public async getAuthUserFromKeyId(keyId: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey | null; } | null> { const key = await publicKeyCache.fetch( keyId, async () => { const key = await UserPublickeys.findOneBy({ keyId, }); if (key == null) return null; return key; }, true, (key) => key != null, ); if (key == null) return null; return { user: (await userByIdCache.fetch( key.userId, () => Users.findOneByOrFail({ id: key.userId }), true, )) as CacheableRemoteUser, key, }; } /** * AP Actor id => Misskey User and Key */ public async getAuthUserFromApId(uri: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey | null; } | null> { const user = (await resolvePerson(uri)) as CacheableRemoteUser; if (user == null) return null; const key = await publicKeyByUserIdCache.fetch( user.id, () => UserPublickeys.findOneBy({ userId: user.id }), true, (v) => v != null, ); return { user, key, }; } public async refetchPublicKeyForApId(user: CacheableRemoteUser): Promise { try { await updatePerson(user.uri!, undefined, undefined, user); let key = await UserPublickeys.findOneBy({ userId: user.id }); if (key != null) { await publicKeyByUserIdCache.set(user.id, key); } return key; } catch { return null; } } } subscriber.on("message", async (_, data) => { const obj = JSON.parse(data); if (obj.channel === "internal") { const { type, body } = obj.message; switch (type) { case "remoteUserDeleted": case "localUserDeleted": { const toDelete = Array.from(await publicKeyByUserIdCache.getAll()) .filter((v) => v[1]?.userId === body.id) .map((v) => v[0]); const toDeleteKey = Array.from(await publicKeyCache.getAll()) .filter((v) => v[1]?.userId === body.id) .map((v) => v[0]); await publicKeyByUserIdCache.delete(...toDelete); await publicKeyCache.delete(...toDeleteKey); break; } default: break; } } });