commit ad4e0f8842f7eb551e035af9d28b24905ac603bb
parent b54096e3dc1da4aa4175982dd17ac3300e56efd8
Author: MTRNord <mtrnord1@gmail.com>
Date: Sun, 9 Jun 2024 17:25:28 +0200
Migrate to meilisearch
Diffstat:
9 files changed, 413 insertions(+), 111 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -2,4 +2,8 @@ dist
node_modules
output.pdf
storage
-db.msp
-\ No newline at end of file
+db.msp
+config.yaml
+meilisearch*
+dumps
+data.ms
+\ No newline at end of file
diff --git a/config.example.yaml b/config.example.yaml
@@ -0,0 +1,4 @@
+homeserverUrl: "https://matrix.midnightthoughts.space"
+accessToken: ""
+indexer:
+ filePath: "./db.msp"
+\ No newline at end of file
diff --git a/package.json b/package.json
@@ -15,10 +15,11 @@
"typescript": "^5.4.5"
},
"dependencies": {
- "@orama/orama": "^2.0.19",
- "@orama/plugin-data-persistence": "^2.0.19",
"@react-pdf/renderer": "^3.4.4",
+ "@sinclair/typebox": "^0.32.31",
"matrix-bot-sdk": "^0.7.1",
- "react": "^18.3.1"
+ "meilisearch": "^0.40.0",
+ "react": "^18.3.1",
+ "yaml": "^2.4.5"
}
}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -8,21 +8,24 @@ importers:
.:
dependencies:
- '@orama/orama':
- specifier: ^2.0.19
- version: 2.0.19
- '@orama/plugin-data-persistence':
- specifier: ^2.0.19
- version: 2.0.19
'@react-pdf/renderer':
specifier: ^3.4.4
version: 3.4.4(react@18.3.1)
+ '@sinclair/typebox':
+ specifier: ^0.32.31
+ version: 0.32.31
matrix-bot-sdk:
specifier: ^0.7.1
version: 0.7.1
+ meilisearch:
+ specifier: ^0.40.0
+ version: 0.40.0
react:
specifier: ^18.3.1
version: 18.3.1
+ yaml:
+ specifier: ^2.4.5
+ version: 2.4.5
devDependencies:
'@types/react':
specifier: ^18.3.3
@@ -41,17 +44,6 @@ packages:
resolution: {integrity: sha512-JXyrHuCVMydUGgSetWsfqbbvHj3aUMOX5TUghlMtLFromyEu7wIsNgYt7PjJ+k3WdF4GVABRy4P6GNjaEMy2uA==}
engines: {node: '>= 16'}
- '@msgpack/msgpack@2.8.0':
- resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
- engines: {node: '>= 10'}
-
- '@orama/orama@2.0.19':
- resolution: {integrity: sha512-9nlF5iYUghewRt2TDmAJO5TN4hYFPhql4d2ZAdJYJtGSIi2BZjMNQIqQ874w0q/rur67wp0AWLb020EakEN0aA==}
- engines: {node: '>= 16.0.0'}
-
- '@orama/plugin-data-persistence@2.0.19':
- resolution: {integrity: sha512-u/ligext2UZuvfM3Nm1JckL7Y6MbT85rXODKtJzZuPdpc2+hilGon9Fmkp00ENBx+dAtzcK+3Q00AY1V5T3Mfg==}
-
'@react-pdf/fns@2.2.1':
resolution: {integrity: sha512-s78aDg0vDYaijU5lLOCsUD+qinQbfOvcNeaoX9AiE7+kZzzCo6B/nX+l48cmt9OosJmvZvE9DWR9cLhrhOi2pA==}
@@ -93,6 +85,9 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+ '@sinclair/typebox@0.32.31':
+ resolution: {integrity: sha512-rYB0tgGHawpom3ZwwsGidvI0NI+W/rRHu1dyyO1KlIoH8iMdg3esSnYQxQtyJ8eflhqxmzEV7Nu8zT4JY7CHKw==}
+
'@swc/helpers@0.4.14':
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
@@ -324,9 +319,6 @@ packages:
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
- dpack@0.6.22:
- resolution: {integrity: sha512-WGPNlW2OAE7Bj0eODMpAHUcEqxrlg01e9OFZDxQodminIgC194/cRHT7K04Z1j7AUEWTeeplYGrIv/xRdwU9Hg==}
-
ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
@@ -568,6 +560,9 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
+ meilisearch@0.40.0:
+ resolution: {integrity: sha512-BoRhQMr2mBFLEeCfsvPluksGb01IaOiWvV3Deu3iEY+yYJ4jdGTu+IQi5FCjKlNQ7/TMWSN2XUToSgvH1tj0BQ==}
+
merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
@@ -919,6 +914,11 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ yaml@2.4.5:
+ resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==}
+ engines: {node: '>= 14'}
+ hasBin: true
+
yoga-layout@2.0.1:
resolution: {integrity: sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q==}
@@ -935,16 +935,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@msgpack/msgpack@2.8.0': {}
-
- '@orama/orama@2.0.19': {}
-
- '@orama/plugin-data-persistence@2.0.19':
- dependencies:
- '@msgpack/msgpack': 2.8.0
- '@orama/orama': 2.0.19
- dpack: 0.6.22
-
'@react-pdf/fns@2.2.1':
dependencies:
'@babel/runtime': 7.24.7
@@ -1057,6 +1047,8 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
+ '@sinclair/typebox@0.32.31': {}
+
'@swc/helpers@0.4.14':
dependencies:
tslib: 2.6.3
@@ -1304,8 +1296,6 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
- dpack@0.6.22: {}
-
ecc-jsbn@0.1.2:
dependencies:
jsbn: 0.1.1
@@ -1593,6 +1583,12 @@ snapshots:
media-typer@0.3.0: {}
+ meilisearch@0.40.0:
+ dependencies:
+ cross-fetch: 3.1.8
+ transitivePeerDependencies:
+ - encoding
+
merge-descriptors@1.0.1: {}
methods@1.1.2: {}
@@ -1948,4 +1944,6 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
+ yaml@2.4.5: {}
+
yoga-layout@2.0.1: {}
diff --git a/src/bot.ts b/src/bot.ts
@@ -1,26 +1,81 @@
-import { MatrixClient, RustSdkCryptoStorageProvider, SimpleFsStorageProvider } from "matrix-bot-sdk";
+import { MatrixClient, RustSdkCryptoStorageProvider, SimpleFsStorageProvider, RustSdkCryptoStoreType } from "matrix-bot-sdk";
import Indexer from "./indexer.js";
+import Config from "./config.js";
+import { GetRoomMessagesRequest, GetRoomMessagesResponse, Received, RoomEventFilter, Event } from "./matrix.js";
-const storageProvider = new SimpleFsStorageProvider("./storage/bot.json");
-const cryptoProvider = new RustSdkCryptoStorageProvider("./storage/crypto");
+const directions = { forward: "f", reverse: "b" } as const;
+// Pending https://github.com/turt2live/matrix-bot-sdk/issues/250
+async function* getRoomEvents(
+ client: MatrixClient,
+ room: string,
+ direction: "forward" | "reverse",
+ filter?: RoomEventFilter
+): AsyncGenerator<Received<Event>[], void, void> {
+ const path = `/_matrix/client/v3/rooms/${encodeURIComponent(room)}/messages`;
+ const base: GetRoomMessagesRequest = {
+ ...(direction && { dir: directions[direction] }),
+ ...(filter && { filter: JSON.stringify(filter) }),
+ };
-//TODO: get these from a config or env vars
-const homeserverUrl = "https://matrix.org";
-const accessToken = "";
-const indexer = new Indexer();
-await indexer.init();
+ let from: string | undefined;
+ do {
+ const query: GetRoomMessagesRequest = { ...base, ...(from && { from }) };
+ const response: GetRoomMessagesResponse = await client.doRequest("GET", path, query);
+ from = response.end;
+ yield response.chunk;
+ } while (from);
+}
-const client = new MatrixClient(homeserverUrl, accessToken, storageProvider, cryptoProvider);
+// Ensures no matrix IDs are in the content.body
+function cleanContent(content: any) {
+ if (typeof content !== "object") return content;
+ if (content.body) {
+ content.body = content.body.replace(/@[a-zA-Z0-9_\-\.=]+:[a-zA-Z0-9\-.]+/g, "<mxid>");
+ }
+ return content;
+}
-client.on("room.message", async (roomId, event) => {
- if (event["content"]["msgtype"] === "m.text") {
- const body = event["content"]["body"];
- console.info(`Received message in room ${roomId}: ${body}`);
+async function run() {
- await indexer.insert(event["content"]);
- }
-});
+ const storageProvider = new SimpleFsStorageProvider("./storage/bot.json");
+ const cryptoProvider = new RustSdkCryptoStorageProvider("./storage/crypto", RustSdkCryptoStoreType.Sqlite);
+
+ const config = new Config();
+ const homeserverUrl = config.homeserverUrl;
+ const accessToken = config.accessToken;
+ const indexer = new Indexer();
+
+ const client = new MatrixClient(homeserverUrl, accessToken, storageProvider, cryptoProvider);
+
+ client.on("room.message", async (roomId, event) => {
+ if (event["content"]["msgtype"] === "m.text") {
+ console.info(`Received message in room ${roomId}`);
-client.start().then(() => {
+ const res = await indexer.insert({ id: event["event_id"].replace("$", "").replace(":", "_").replace(".", "_"), sender: event["sender"], content: cleanContent(event["content"]), room_id: roomId });
+ console.info(`Indexed message:`, res);
+ }
+ });
+
+ await client.start();
console.info("Bot started!");
-});
-\ No newline at end of file
+
+ // TODO: Only do this on the first run
+ console.info("Indexing existing messages")
+ const joined_rooms = await client.getJoinedRooms();
+ for (const room of joined_rooms) {
+ const pages = getRoomEvents(client, room, "reverse", { types: ["m.room.message"] });
+ for await (const page of pages) {
+ for (const event of page) {
+ if (await client.crypto.isRoomEncrypted(room)) {
+ await client.crypto.onRoomEvent(room, event);
+ } else {
+ const res = await indexer.insert({ id: event["event_id"].replace("$", "").replace(":", "_").replace(".", "_"), sender: event["sender"], content: cleanContent(event["content"]), room_id: room });
+ console.info(`Indexed message:`, res);
+ }
+ }
+ }
+ }
+
+}
+
+await run();
+\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
@@ -0,0 +1,62 @@
+import { Type, type Static } from '@sinclair/typebox';
+import { Value } from '@sinclair/typebox/value';
+import fs from 'node:fs';
+import YAML from 'yaml';
+
+// The config schema
+const ConfigSchema = Type.Object({
+ homeserverUrl: Type.String(),
+ accessToken: Type.String(),
+});
+
+export type ConfigSchema = Static<typeof ConfigSchema>;
+
+// A config loader with strict schema types using typebox
+export default class Config {
+ private config: ConfigSchema;
+
+ constructor() {
+ // Load the config from the env and use the config for the rest.
+ // Then verify the config against the schema
+ const config = {
+ homeserverUrl: process.env.HOMESERVER_URL,
+ accessToken: process.env.ACCESS_TOKEN,
+ };
+
+ // load the config file async and merge it with the env
+ const configFile = fs.readFileSync('config.yaml', 'utf-8');
+ const configFromFile = YAML.parse(configFile);
+ const mergedConfig = {
+ ...config,
+ ...configFromFile
+ };
+
+ // Verify the config against the schema
+ if (!Value.Check(ConfigSchema, mergedConfig)) {
+ throw new Error('Invalid config');
+ }
+ this.config = mergedConfig;
+ }
+
+ public get homeserverUrl(): string {
+ return this.config.homeserverUrl;
+ }
+
+ public get accessToken(): string {
+ return this.config.accessToken;
+ }
+
+ public set homeserverUrl(value: string) {
+ this.config.homeserverUrl = value;
+ this.save();
+ }
+
+ public set accessToken(value: string) {
+ this.config.accessToken = value;
+ this.save();
+ }
+
+ public save() {
+ fs.writeFileSync('config.yaml', YAML.stringify(this.config));
+ }
+}
+\ No newline at end of file
diff --git a/src/indexer.ts b/src/indexer.ts
@@ -1,5 +1,4 @@
-import { AnyOrama, create, insert, search } from '@orama/orama';
-import { persistToFile, restoreFromFile } from '@orama/plugin-data-persistence/server';
+import { Hits, MeiliSearch } from 'meilisearch';
import fs from 'node:fs';
/* This is the indexer. It is a wrapper around orama.
@@ -10,54 +9,32 @@ import fs from 'node:fs';
*
*/
export default class Indexer {
- private db?: AnyOrama;
+ private client = new MeiliSearch({
+ host: 'http://localhost:7700',
+ apiKey: 'aSampleMasterKey'
+ });
- public async init() {
- const filePath = 'db.msp';
- // Check if database exists
- if (!fs.existsSync(filePath)) {
- // Create database
- this.db = await create({
- schema: {
- room_id: 'string',
- text: {
- body: 'string',
- formatted_body: 'string',
- "m.mentions": {
- user_ids: 'string[]'
- },
- "m.relates_to": {
- "m.in_reply_to": {
- event_id: 'string'
- }
- },
- }
- },
- components: {
- tokenizer: {
- stemmerSkipProperties: ['room_id']
- }
- }
- });
- await persistToFile(this.db, 'binary', filePath);
- } else {
- // Load database
- this.db = await restoreFromFile('binary', filePath);
- }
- }
+ constructor() { }
// TODO: Properly type the data
public async insert(data: any) {
- if (!this.db) {
- throw new Error('Database not initialized');
- }
- return await insert(this.db, data);
+ const index = this.client.index('text');
+ return await index.addDocuments([data], { primaryKey: 'id' });
}
public async search(query: string) {
- if (!this.db) {
- throw new Error('Database not initialized');
+ const index = this.client.index('text');
+ let results: Hits<Record<string, any>> = [];
+ let page = 1;
+ // Keep searching until we got everything
+ let response = await index.search(query, { page: page });
+ results = results.concat(response.hits);
+ while (page < response.totalPages) {
+ response = await index.search(query, { page: page });
+ results = results.concat(response.hits);
+ page++;
}
- return await search(this.db, { term: query });
+
+ return { hits: results, estimatedTotalHits: results.length };
}
}
\ No newline at end of file
diff --git a/src/matrix.ts b/src/matrix.ts
@@ -0,0 +1,192 @@
+import type {
+ MatrixError, PowerLevelsEventContent as PowerLevels,
+ RoomCreateOptions as RoomCreateFullOptions,
+} from "matrix-bot-sdk";
+
+export interface RoomCreateOptions extends RoomCreateFullOptions {
+ preset?: Exclude<NonNullable<RoomCreateFullOptions["preset"]>, "trusted_private_chat">;
+}
+
+//
+// Events
+//
+
+interface IEvent<T extends string, C> {
+ type: T;
+ event_id: string;
+ content: C;
+}
+
+export interface IStateEvent<T extends string, C> extends IEvent<T, C> {
+ state_key: string;
+}
+
+export type Received<E extends Event> = E & {
+ content: E["content"] | {} /* redacted */;
+ origin_server_ts: number;
+ room_id: string;
+ sender: string;
+};
+
+export type MessageEvent<T = unknown> = (
+ | IEvent<
+ "m.reaction",
+ { "m.relates_to": { rel_type: "m.annotation"; event_id: string; key: string } }
+ >
+ | IEvent<
+ "m.room.message",
+ {
+ body: string;
+ msgtype: "m.notice" | "m.text";
+ "m.relates_to"?: { rel_type: "m.replace"; event_id: string };
+ } & ({} | { format: "org.matrix.custom.html"; formatted_body: string })
+ >
+ | (IEvent<"m.room.redaction", { reason?: string }> & { redacts: string })
+) & { type: T };
+
+type WidgetContent = {
+ creatorUserId: string;
+ name: string;
+ avatar_url?: string;
+} & (
+ | { type: "customwidget"; url: string }
+ | {
+ type: "jitsi";
+ url: string;
+ data: { domain: string; conferenceId: string; roomName: string };
+ }
+ );
+
+export type StateEvent<T = unknown> = (
+ | IStateEvent<"im.vector.modular.widgets", {} | WidgetContent>
+ | IStateEvent<
+ "io.element.widgets.layout",
+ {
+ widgets: Record<
+ string,
+ { index: number; container: "top"; height: number; width: number }
+ >;
+ }
+ >
+ | IStateEvent<"m.room.avatar", { url: string }>
+ | IStateEvent<"m.room.canonical_alias", { alias: string; alt_aliases?: string[] }>
+ | IStateEvent<"m.room.guest_access", { guest_access: "can_join" | "forbidden" }>
+ | IStateEvent<
+ "m.room.history_visibility",
+ { history_visibility: "invited" | "joined" | "shared" | "world_readable" }
+ >
+ | IStateEvent<
+ "m.room.join_rules",
+ | { join_rule: "invite" | "knock" | "private" | "public" }
+ | {
+ join_rule: "knock_restricted" | "restricted";
+ allow: { type: "m.room_membership"; room_id: string }[];
+ }
+ >
+ | IStateEvent<
+ "m.room.member",
+ { membership: "ban" | "invite" | "join" | "knock" | "leave" }
+ >
+ | IStateEvent<"m.room.name", { name: string }>
+ | IStateEvent<"m.room.power_levels", PowerLevels>
+ | IStateEvent<"m.room.topic", { topic: string }>
+) & { type: T };
+
+export type Event = MessageEvent | StateEvent;
+
+export type StateEventInput = Omit<StateEvent, "event_id" | "sender" | "state_key"> &
+ Partial<Pick<StateEvent, "state_key">>;
+
+//
+// Client API
+//
+
+export interface GetRoomMessagesRequest {
+ dir: "b" | "f";
+ filter?: string; // RoomEventFilter
+ from?: string;
+}
+
+export interface GetRoomMessagesResponse {
+ chunk: Received<Event>[];
+ end?: string;
+}
+
+export interface RoomEventFilter {
+ not_senders?: string[];
+ not_types?: string[];
+ senders?: string[];
+ types?: string[];
+}
+
+export interface Sync {
+ rooms?: {
+ join?: {
+ [id: string]: {
+ state: { events: Received<StateEvent>[] };
+ timeline: { events: Received<Event>[] };
+ };
+ };
+ };
+}
+
+// Workaround for turt2live/matrix-bot-sdk#197
+export const orNone = (error: MatrixError) => {
+ if (error.errcode === "M_NOT_FOUND") return undefined;
+
+ throw error;
+};
+
+//
+// Server constants
+//
+
+const defaultState: Record<string, StateEvent["content"]> = {
+ "m.room.guest_access/": { guest_access: "forbidden" },
+};
+
+export const moderatorLevel = 50;
+
+// As at https://github.com/matrix-org/synapse/blob/v1.67.0/synapse/handlers/room.py#L123
+export const resolvePreset = (
+ preset: RoomCreateOptions["preset"]
+): Pick<RoomCreateOptions, "initial_state"> => {
+ switch (preset) {
+ case undefined:
+ return {};
+ case "public_chat":
+ return {
+ initial_state: [
+ { type: "m.room.guest_access", content: { guest_access: "forbidden" } },
+ {
+ type: "m.room.history_visibility",
+ content: { history_visibility: "shared" },
+ },
+ { type: "m.room.join_rules", content: { join_rule: "public" } },
+ ],
+ };
+ case "private_chat":
+ return {
+ initial_state: [
+ { type: "m.room.guest_access", content: { guest_access: "can_join" } },
+ {
+ type: "m.room.history_visibility",
+ content: { history_visibility: "shared" },
+ },
+ { type: "m.room.join_rules", content: { join_rule: "invite" } },
+ ],
+ };
+ default:
+ throw new Error(`Not implemented for preset ${preset}`);
+ }
+};
+
+//
+// Helpers
+//
+
+export const isStateEvent = (event: Event): event is StateEvent => "state_key" in event;
+
+export const isUserId = (text: string): boolean => /^@[-.\w]+:[-.\w]+$/.test(text);
+
+export const permalinkPattern = /https:\/\/matrix\.to\/#\/([!#@][-.\w]+:[-.\w]+)/;
diff --git a/src/renderer.tsx b/src/renderer.tsx
@@ -1,7 +1,8 @@
import React, { PropsWithChildren } from 'react';
import ReactPDF, { Font, Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';
import Indexer from './indexer.js';
-import { Results, AnyDocument } from '@orama/orama';
+import Config from './config.js';
+import { Hits, SearchParams, SearchResponse } from 'meilisearch';
Font.registerEmojiSource({
format: 'png',
@@ -20,11 +21,11 @@ const styles = StyleSheet.create({
})
const indexer = new Indexer();
-await indexer.init();
-await indexer.insert({ text: { body: "Hello, World with test!" } });
+
+const borderRadius = 5;
// A pdf renderer
-const MessageDocument = ({ query, queryResults }: PropsWithChildren<{ query: string, queryResults: Results<AnyDocument> }>) => {
+const MessageDocument = ({ query, queryResults }: PropsWithChildren<{ query: string, queryResults: { hits: Hits<Record<string, any>>; estimatedTotalHits: number; } }>) => {
return (
<Document
title={`Search results for: "${query}"`}
@@ -34,19 +35,23 @@ const MessageDocument = ({ query, queryResults }: PropsWithChildren<{ query: str
<Page size="A4" style={styles.page}>
<View>
<Text>Search results for: {query}</Text>
- <Text>Results: {queryResults.count}</Text>
+ <Text>Results: {queryResults.estimatedTotalHits}</Text>
{queryResults.hits.map((result, index) => (
- <View key={index}>
- <Text>{JSON.stringify(result.document, null, 2)}</Text>
+ <View key={index} style={{ flexDirection: 'column', margin: 10, padding: 5, backgroundColor: '#e6e6e6', gap: 25, borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius, borderBottomLeftRadius: borderRadius, borderBottomRightRadius: borderRadius }}>
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between' }} >
+ <Text>{result.room_id}</Text>
+ <Text>{result.sender}</Text>
+ </View>
+ <Text>{result.content.body}</Text>
</View>
))}
</View>
- <View style={{ flexDirection: 'row', justifyContent: 'space-between' }} >
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }} fixed>
<Text render={({ pageNumber, totalPages }) => (
`${pageNumber} / ${totalPages}`
- )} fixed />
- <Text fixed>
+ )} />
+ <Text>
Matrix Search Engine
</Text>
</View>
@@ -57,7 +62,9 @@ const MessageDocument = ({ query, queryResults }: PropsWithChildren<{ query: str
export async function renderPDFToDisk(query: string) {
const queryResults = await indexer.search(query);
+ //console.log(`Search results:`, queryResults);
ReactPDF.renderToFile(<MessageDocument query={query} queryResults={queryResults} />, `output.pdf`);
}
-await renderPDFToDisk("test");
-\ No newline at end of file
+await renderPDFToDisk("test");
+console.log("PDF rendered to disk");
+\ No newline at end of file