From aa0813fef8a3c112e3eeb8d665ae1586b71e90a6 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 2 Dec 2023 18:11:30 -1000 Subject: [PATCH] feat: Add Meilisearch integration, begin work on search endpoint --- bun.lockb | Bin 345808 -> 349144 bytes cli.ts | 77 ++++++++- config/config.example.toml | 6 + database/entities/Notification.ts | 2 +- database/entities/Status.ts | 127 +++++++++++++- index.ts | 5 + package.json | 4 + .../20231202001242_add_source/migration.sql | 2 + prisma/schema.prisma | 1 + server/api/api/v1/statuses/[id]/index.ts | 155 +++++++++++++++++- server/api/api/v1/statuses/[id]/source.ts | 49 ++++++ server/api/api/v2/search/index.ts | 60 +++++++ utils/config.ts | 13 ++ utils/meilisearch.ts | 111 +++++++++++++ 14 files changed, 605 insertions(+), 7 deletions(-) create mode 100644 prisma/migrations/20231202001242_add_source/migration.sql create mode 100644 server/api/api/v1/statuses/[id]/source.ts create mode 100644 server/api/api/v2/search/index.ts create mode 100644 utils/meilisearch.ts diff --git a/bun.lockb b/bun.lockb index ae9680ac77bc0a147c4a52dc86b8a85ddf415c7e..eda9a2f4b8f0397e42b323658663418e9c3ffefd 100755 GIT binary patch delta 67769 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 z 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 O)I3~eN1h#wR{R(AaPF`G delta 66378 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 F{{;@f6)6A! diff --git a/cli.ts b/cli.ts index a42f4d77..a21d9000 100644 --- a/cli.ts +++ b/cli.ts @@ -3,6 +3,7 @@ import chalk from "chalk"; import { client } from "~database/datasource"; import { createNewLocalUser } from "~database/entities/User"; import Table from "cli-table"; +import { rebuildSearchIndexes, SonicIndexType } from "@meilisearch"; const args = process.argv; @@ -86,7 +87,20 @@ ${chalk.bold("Commands:")} ${chalk.bold("Example:")} ${chalk.bgGray( `bun cli note search hello` )} - + ${alignDots(chalk.blue("index"), 24)} Manage user and status indexes + ${alignDots(chalk.blue("rebuild"))} Rebuild the index + ${alignDotsSmall( + chalk.green("batch-size") + )} The number of items to index at once (optional, default 100) + ${alignDotsSmall( + chalk.yellow("--statuses") + )} Only rebuild the statuses index (optional) + ${alignDotsSmall( + chalk.yellow("--users") + )} Only rebuild the users index (optional) + ${chalk.bold("Example:")} ${chalk.bgGray( + `bun cli index rebuild --users 200` + )} `; if (args.length < 3) { @@ -504,10 +518,71 @@ switch (command) { console.log(`Unknown command ${chalk.blue(command)}`); break; } + break; + } + case "index": { + switch (args[3]) { + case "rebuild": { + const statuses = args.includes("--statuses"); + const users = args.includes("--users"); + const argsWithoutFlags = args.filter( + arg => !arg.startsWith("--") + ); + + const batchSize = Number(argsWithoutFlags[4]) || 100; + + const neither = !statuses && !users; + + if (statuses || neither) { + console.log( + `${chalk.yellow(`⚠`)} ${chalk.bold( + `Rebuilding Meilisearch index for statuses` + )}` + ); + + await rebuildSearchIndexes( + [SonicIndexType.Statuses], + batchSize + ); + + console.log( + `${chalk.green(`✓`)} ${chalk.bold( + `Meilisearch index for statuses rebuilt` + )}` + ); + } + + if (users || neither) { + console.log( + `${chalk.yellow(`⚠`)} ${chalk.bold( + `Rebuilding Meilisearch index for users` + )}` + ); + + await rebuildSearchIndexes( + [SonicIndexType.Accounts], + batchSize + ); + + console.log( + `${chalk.green(`✓`)} ${chalk.bold( + `Meilisearch index for users rebuilt` + )}` + ); + } + + break; + } + default: + console.log(`Unknown command ${chalk.blue(command)}`); + break; + } break; } default: console.log(`Unknown command ${chalk.blue(command)}`); break; } + +process.exit(0); diff --git a/config/config.example.toml b/config/config.example.toml index 603eb05a..8af0ecd9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -18,6 +18,12 @@ password = "" database = 1 enabled = false +[meilisearch] +host = "localhost" +port = 40007 +api_key = "" +enabled = true + [http] base_url = "https://lysand.social" bind = "http://localhost" diff --git a/database/entities/Notification.ts b/database/entities/Notification.ts index f6bd4c00..80399f30 100644 --- a/database/entities/Notification.ts +++ b/database/entities/Notification.ts @@ -13,7 +13,7 @@ export const notificationToAPI = async ( ): Promise => { return { account: userToAPI(notification.account), - created_at: notification.createdAt.toISOString(), + created_at: new Date(notification.createdAt).toISOString(), id: notification.id, type: notification.type, status: notification.status diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 4b1cd2a8..d743e715 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -24,6 +24,10 @@ import type { APIStatus } from "~types/entities/status"; import { applicationToAPI } from "./Application"; import { attachmentToAPI } from "./Attachment"; import type { APIAttachment } from "~types/entities/attachment"; +import { sanitizeHtml } from "@sanitization"; +import { parse } from "marked"; +import linkifyStr from "linkify-string"; +import linkifyHtml from "linkify-html"; const config = getConfig(); @@ -303,7 +307,7 @@ export const createNewStatus = async (data: { visibility: APIStatus["visibility"]; sensitive: boolean; spoiler_text: string; - emojis: Emoji[]; + emojis?: Emoji[]; content_type?: string; uri?: string; mentions?: User[]; @@ -320,6 +324,8 @@ export const createNewStatus = async (data: { let mentions = data.mentions || []; + // TODO: Parse emojis + // Get list of mentioned users if (mentions.length === 0) { mentions = await client.user.findMany({ @@ -335,17 +341,36 @@ export const createNewStatus = async (data: { }); } + let formattedContent; + + // Get HTML version of content + if (data.content_type === "text/markdown") { + formattedContent = linkifyHtml(await sanitizeHtml(parse(data.content))); + } else if (data.content_type === "text/x.misskeymarkdown") { + // Parse as MFM + } else { + // Parse as plaintext + formattedContent = linkifyStr(data.content); + + // Split by newline and add

tags + formattedContent = formattedContent + .split("\n") + .map(line => `

${line}

`) + .join("\n"); + } + let status = await client.status.create({ data: { authorId: data.account.id, applicationId: data.application?.id, - content: data.content, + content: formattedContent, + contentSource: data.content, contentType: data.content_type, visibility: data.visibility, sensitive: data.sensitive, spoilerText: data.spoiler_text, emojis: { - connect: data.emojis.map(emoji => { + connect: data.emojis?.map(emoji => { return { id: emoji.id, }; @@ -405,6 +430,102 @@ export const createNewStatus = async (data: { return status; }; +export const editStatus = async ( + status: StatusWithRelations, + data: { + content: string; + visibility?: APIStatus["visibility"]; + sensitive: boolean; + spoiler_text: string; + emojis?: Emoji[]; + content_type?: string; + uri?: string; + mentions?: User[]; + media_attachments?: string[]; + } +) => { + // Get people mentioned in the content (match @username or @username@domain.com mentions + const mentionedPeople = + data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? []; + + let mentions = data.mentions || []; + + // TODO: Parse emojis + + // Get list of mentioned users + if (mentions.length === 0) { + mentions = await client.user.findMany({ + where: { + OR: mentionedPeople.map(person => ({ + username: person.split("@")[1], + instance: { + base_url: person.split("@")[2], + }, + })), + }, + include: userRelations, + }); + } + + let formattedContent; + + // Get HTML version of content + if (data.content_type === "text/markdown") { + formattedContent = linkifyHtml(await sanitizeHtml(parse(data.content))); + } else if (data.content_type === "text/x.misskeymarkdown") { + // Parse as MFM + } else { + // Parse as plaintext + formattedContent = linkifyStr(data.content); + + // Split by newline and add

tags + formattedContent = formattedContent + .split("\n") + .map(line => `

${line}

`) + .join("\n"); + } + + const newStatus = await client.status.update({ + where: { + id: status.id, + }, + data: { + content: formattedContent, + contentSource: data.content, + contentType: data.content_type, + visibility: data.visibility, + sensitive: data.sensitive, + spoilerText: data.spoiler_text, + emojis: { + connect: data.emojis?.map(emoji => { + return { + id: emoji.id, + }; + }), + }, + attachments: data.media_attachments + ? { + connect: data.media_attachments.map(attachment => { + return { + id: attachment, + }; + }), + } + : undefined, + mentions: { + connect: mentions.map(mention => { + return { + id: mention.id, + }; + }), + }, + }, + include: statusAndUserRelations, + }); + + return newStatus; +}; + export const isFavouritedBy = async (status: Status, user: User) => { return !!(await client.like.findFirst({ where: { diff --git a/index.ts b/index.ts index 38a9de04..031db5b3 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,7 @@ import { client } from "~database/datasource"; import type { PrismaClientInitializationError } from "@prisma/client/runtime/library"; import { HookTypes, Server } from "~plugins/types"; import { initializeRedisCache } from "@redis"; +import { connectMeili } from "@meilisearch"; const timeAtStart = performance.now(); const server = new Server(); @@ -36,6 +37,10 @@ if (!(await requests_log.exists())) { const redisCache = await initializeRedisCache(); +if (config.meilisearch.enabled) { + await connectMeili(); +} + if (redisCache) { client.$use(redisCache); } diff --git a/package.json b/package.json index 224545b0..d8ce8262 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,11 @@ "iso-639-1": "^3.1.0", "isomorphic-dompurify": "^1.10.0", "jsonld": "^8.3.1", + "linkify-html": "^4.1.3", + "linkify-string": "^4.1.3", + "linkifyjs": "^4.1.3", "marked": "^9.1.2", + "meilisearch": "^0.36.0", "prisma": "^5.6.0", "prisma-redis-middleware": "^4.8.0", "semver": "^7.5.4", diff --git a/prisma/migrations/20231202001242_add_source/migration.sql b/prisma/migrations/20231202001242_add_source/migration.sql new file mode 100644 index 00000000..272e2eb2 --- /dev/null +++ b/prisma/migrations/20231202001242_add_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Status" ADD COLUMN "contentSource" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 86874950..bbba2240 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -103,6 +103,7 @@ model Status { isReblog Boolean content String @default("") contentType String @default("text/plain") + contentSource String @default("") visibility String inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull) inReplyToPostId String? @db.Uuid diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index ad7b7cee..60722cbb 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -1,8 +1,13 @@ import { applyConfig } from "@api"; +import { getConfig } from "@config"; +import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; +import { sanitizeHtml } from "@sanitization"; import type { MatchedRoute } from "bun"; +import { parse } from "marked"; import { client } from "~database/datasource"; import { + editStatus, isViewableByUser, statusAndUserRelations, statusToAPI, @@ -11,7 +16,7 @@ import { getFromRequest } from "~database/entities/User"; import type { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ - allowedMethods: ["GET", "DELETE"], + allowedMethods: ["GET", "DELETE", "PUT"], ratelimits: { max: 100, duration: 60, @@ -19,7 +24,7 @@ export const meta: APIRouteMeta = applyConfig({ route: "/api/v1/statuses/:id", auth: { required: false, - requiredOnMethods: ["DELETE"], + requiredOnMethods: ["DELETE", "PUT"], }, }); @@ -39,6 +44,8 @@ export default async ( include: statusAndUserRelations, }); + const config = getConfig(); + // Check if user is authorized to view this status (if it's private) if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); @@ -69,6 +76,150 @@ export default async ( }, 200 ); + } else if (req.method == "PUT") { + if (status.authorId !== user?.id) { + return errorResponse("Unauthorized", 401); + } + + const { + status: statusText, + content_type, + "poll[expires_in]": expires_in, + "poll[options][]": options, + "media_ids[]": media_ids, + spoiler_text, + sensitive, + } = await parseRequest<{ + status?: string; + spoiler_text?: string; + sensitive?: boolean; + language?: string; + content_type?: string; + "media_ids[]"?: string[]; + "poll[options][]"?: string[]; + "poll[expires_in]"?: number; + "poll[multiple]"?: boolean; + "poll[hide_totals]"?: boolean; + }>(req); + + // TODO: Add Poll support + // Validate status + if (!statusText && !(media_ids && media_ids.length > 0)) { + return errorResponse( + "Status is required unless media is attached", + 422 + ); + } + + // Validate media_ids + if (media_ids && !Array.isArray(media_ids)) { + return errorResponse("Media IDs must be an array", 422); + } + + // Validate poll options + if (options && !Array.isArray(options)) { + return errorResponse("Poll options must be an array", 422); + } + + if (options && options.length > 4) { + return errorResponse("Poll options must be less than 5", 422); + } + + if (media_ids && media_ids.length > 0) { + // Disallow poll + if (options) { + return errorResponse("Cannot attach poll to media", 422); + } + if (media_ids.length > 4) { + return errorResponse("Media IDs must be less than 5", 422); + } + } + + if (options && options.length > config.validation.max_poll_options) { + return errorResponse( + `Poll options must be less than ${config.validation.max_poll_options}`, + 422 + ); + } + + if ( + options && + options.some( + option => option.length > config.validation.max_poll_option_size + ) + ) { + return errorResponse( + `Poll options must be less than ${config.validation.max_poll_option_size} characters`, + 422 + ); + } + + if (expires_in && expires_in < config.validation.min_poll_duration) { + return errorResponse( + `Poll duration must be greater than ${config.validation.min_poll_duration} seconds`, + 422 + ); + } + + if (expires_in && expires_in > config.validation.max_poll_duration) { + return errorResponse( + `Poll duration must be less than ${config.validation.max_poll_duration} seconds`, + 422 + ); + } + + let sanitizedStatus: string; + + if (content_type === "text/markdown") { + sanitizedStatus = await sanitizeHtml(parse(statusText ?? "")); + } else if (content_type === "text/x.misskeymarkdown") { + // Parse as MFM + // TODO: Parse as MFM + sanitizedStatus = await sanitizeHtml(parse(statusText ?? "")); + } else { + sanitizedStatus = await sanitizeHtml(statusText ?? ""); + } + + if (sanitizedStatus.length > config.validation.max_note_size) { + return errorResponse( + `Status must be less than ${config.validation.max_note_size} characters`, + 400 + ); + } + + // Check if status body doesnt match filters + if ( + config.filters.note_filters.some( + filter => statusText?.match(filter) + ) + ) { + return errorResponse("Status contains blocked words", 422); + } + + // Check if media attachments are all valid + + const foundAttachments = await client.attachment.findMany({ + where: { + id: { + in: media_ids ?? [], + }, + }, + }); + + if (foundAttachments.length !== (media_ids ?? []).length) { + return errorResponse("Invalid media IDs", 422); + } + + // Update status + const newStatus = await editStatus(status, { + content: sanitizedStatus, + content_type, + media_attachments: media_ids, + spoiler_text: spoiler_text ?? "", + sensitive: sensitive ?? false, + }); + + return jsonResponse(await statusToAPI(newStatus, user)); } return jsonResponse({}); diff --git a/server/api/api/v1/statuses/[id]/source.ts b/server/api/api/v1/statuses/[id]/source.ts new file mode 100644 index 00000000..ae9defbc --- /dev/null +++ b/server/api/api/v1/statuses/[id]/source.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { applyConfig } from "@api"; +import { errorResponse, jsonResponse } from "@response"; +import type { MatchedRoute } from "bun"; +import { client } from "~database/datasource"; +import { createLike } from "~database/entities/Like"; +import { + isViewableByUser, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; +import type { APIRouteMeta } from "~types/api"; +import type { APIStatus } from "~types/entities/status"; + +export const meta: APIRouteMeta = applyConfig({ + allowedMethods: ["GET"], + ratelimits: { + max: 100, + duration: 60, + }, + route: "/api/v1/statuses/:id/source", + auth: { + required: true, + }, +}); + +/** + * Favourite a post + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const id = matchedRoute.params.id; + + const { user } = await getFromRequest(req); + + if (!user) return errorResponse("Unauthorized", 401); + + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); + + // Check if user is authorized to view this status (if it's private) + if (!status || !isViewableByUser(status, user)) + return errorResponse("Record not found", 404); +}; diff --git a/server/api/api/v2/search/index.ts b/server/api/api/v2/search/index.ts new file mode 100644 index 00000000..5e260867 --- /dev/null +++ b/server/api/api/v2/search/index.ts @@ -0,0 +1,60 @@ +import { applyConfig } from "@api"; +import { parseRequest } from "@request"; +import { errorResponse, jsonResponse } from "@response"; +import { getFromRequest } from "~database/entities/User"; +import type { APIRouteMeta } from "~types/api"; + +export const meta: APIRouteMeta = applyConfig({ + allowedMethods: ["GET"], + ratelimits: { + max: 10, + duration: 60, + }, + route: "/api/v2/search", + auth: { + required: false, + oauthPermissions: ["read:search"], + }, +}); + +/** + * Upload new media + */ +export default async (req: Request): Promise => { + const { user } = await getFromRequest(req); + + const { + q, + type, + resolve, + following, + account_id, + max_id, + min_id, + limit, + offset, + } = await parseRequest<{ + q?: string; + type?: string; + resolve?: boolean; + following?: boolean; + account_id?: string; + max_id?: string; + min_id?: string; + limit?: number; + offset?: number; + }>(req); + + if (!user && (resolve || offset)) { + return errorResponse( + "Cannot use resolve or offset without being authenticated", + 401 + ); + } + + return jsonResponse({ + accounts: [], + statuses: [], + hashtags: [], + }); +}; diff --git a/utils/config.ts b/utils/config.ts index 92553c16..f8a9d12c 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -25,6 +25,13 @@ export interface ConfigType { }; }; + meilisearch: { + host: string; + port: number; + api_key: string; + enabled: boolean; + }; + http: { base_url: string; bind: string; @@ -176,6 +183,12 @@ export const configDefaults: ConfigType = { enabled: false, }, }, + meilisearch: { + host: "localhost", + port: 1491, + api_key: "", + enabled: false, + }, instance: { banner: "", description: "", diff --git a/utils/meilisearch.ts b/utils/meilisearch.ts new file mode 100644 index 00000000..ba097d0b --- /dev/null +++ b/utils/meilisearch.ts @@ -0,0 +1,111 @@ +import { getConfig } from "@config"; +import chalk from "chalk"; +import { client } from "~database/datasource"; +import { Meilisearch } from "meilisearch"; + +const config = getConfig(); + +export const meilisearch = new Meilisearch({ + host: `${config.meilisearch.host}:${config.meilisearch.port}`, + apiKey: config.meilisearch.api_key, +}); + +export const connectMeili = async () => { + if (!config.meilisearch.enabled) return; + + if (await meilisearch.isHealthy()) { + console.log( + `${chalk.green(`✓`)} ${chalk.bold(`Connected to Meilisearch`)}` + ); + } else { + console.error( + `${chalk.red(`✗`)} ${chalk.bold( + `Error while connecting to Meilisearch` + )}` + ); + process.exit(1); + } +}; + +export enum SonicIndexType { + Accounts = "accounts", + Statuses = "statuses", +} + +export const getNthDatabaseAccountBatch = ( + n: number, + batchSize = 1000 +): Promise[]> => { + return client.user.findMany({ + skip: n * batchSize, + take: batchSize, + select: { + id: true, + username: true, + displayName: true, + note: true, + }, + }); +}; + +export const getNthDatabaseStatusBatch = ( + n: number, + batchSize = 1000 +): Promise[]> => { + return client.status.findMany({ + skip: n * batchSize, + take: batchSize, + select: { + id: true, + authorId: true, + content: true, + }, + }); +}; + +export const rebuildSearchIndexes = async ( + indexes: SonicIndexType[], + batchSize = 100 +) => { + if (indexes.includes(SonicIndexType.Accounts)) { + // await sonicIngestor.flushc(SonicIndexType.Accounts); + + const accountCount = await client.user.count(); + + for (let i = 0; i < accountCount / batchSize; i++) { + const accounts = await getNthDatabaseAccountBatch(i, batchSize); + + const progress = Math.round((i / (accountCount / batchSize)) * 100); + + console.log(`${chalk.green(`✓`)} ${progress}%`); + + // Sync with Meilisearch + await meilisearch + .index(SonicIndexType.Accounts) + .addDocuments(accounts); + } + + console.log(`${chalk.green(`✓`)} ${chalk.bold(`Done!`)}`); + } + + if (indexes.includes(SonicIndexType.Statuses)) { + // await sonicIngestor.flushc(SonicIndexType.Statuses); + + const statusCount = await client.status.count(); + + for (let i = 0; i < statusCount / batchSize; i++) { + const statuses = await getNthDatabaseStatusBatch(i, batchSize); + + const progress = Math.round((i / (statusCount / batchSize)) * 100); + + console.log(`${chalk.green(`✓`)} ${progress}%`); + + // Sync with Meilisearch + await meilisearch + .index(SonicIndexType.Statuses) + .addDocuments(statuses); + } + + console.log(`${chalk.green(`✓`)} ${chalk.bold(`Done!`)}`); + } +};