matrix-search

A small nodejs script which indexes a whole matrix account into meilisearch and has a pdf rendering UI
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-search.git
Log | Files | Refs | README | LICENSE

renderer.tsx (8045B)


      1 import React, { PropsWithChildren } from 'react';
      2 import ReactPDF, { Font, Page, Text, View, Document, StyleSheet, Link, renderToBuffer } from '@react-pdf/renderer';
      3 import Indexer from './indexer.js';
      4 import { Hits } from 'meilisearch';
      5 import prompts, { PromptObject } from 'prompts';
      6 import { fileURLToPath } from 'node:url';
      7 import { resolve } from 'node:path';
      8 
      9 Font.registerEmojiSource({
     10     format: 'png',
     11     url: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/',
     12 });
     13 
     14 
     15 const styles = StyleSheet.create({
     16     page: {
     17         fontSize: 12,
     18         fontFamily: 'Helvetica',
     19         flexDirection: 'column',
     20         justifyContent: 'space-between',
     21         backgroundColor: '#fff',
     22         padding: 10,
     23     }
     24 })
     25 
     26 const indexer = new Indexer();
     27 
     28 const borderRadius = 2;
     29 
     30 // A pdf renderer
     31 export const MessageDocument = ({ query, room_id, sender, queryResults }: PropsWithChildren<{ query: string, room_id?: string, sender?: string, queryResults: { hits: Hits<Record<string, any>>; estimatedTotalHits: number; } }>) => {
     32     return (
     33         <Document
     34             title={`Search results for: "${query}"`}
     35             producer='Matrix Search Engine'
     36             keywords='matrix'
     37         >
     38             <Page size="A4" style={styles.page}>
     39                 {/* Title page which is centered */}
     40                 <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", height: "100%" }}>
     41                     <View style={{ flexDirection: "column", justifyContent: "center", alignItems: "center" }}>
     42                         <Text style={{ fontSize: 24, textAlign: 'center' }}>Matrix Search Engine</Text>
     43                         <Text style={{ fontSize: 12, textAlign: 'center', marginTop: 5 }}>Search results for: "{query}"</Text>
     44                         {/* Optionally display the other values of room_id and sender*/}
     45                         {room_id !== undefined && <Text style={{ fontSize: 12, textAlign: 'center' }}>Room ID: {room_id}</Text>}
     46                         {sender !== undefined && <Text style={{ fontSize: 12, textAlign: 'center' }}>Sender: {sender}</Text>}
     47                         {/* Number of results */}
     48                         <Text style={{ fontSize: 10, textAlign: 'center', marginTop: 10 }}>Number of Results: {queryResults.estimatedTotalHits}</Text>
     49                         {/* Date of creation */}
     50                         <Text style={{ fontSize: 10, textAlign: 'center', marginTop: 10 }}>Created at: {new Date().toLocaleString()}</Text>
     51                     </View>
     52                 </View>
     53             </Page>
     54             <Page size="A4" style={styles.page}>
     55                 <View>
     56                     <View style={{ fontSize: 10 }}>
     57                         <Text>Search results for: "{query}"</Text>
     58                         <Text>Number of Results: {queryResults.estimatedTotalHits}</Text>
     59                     </View>
     60                     <View>
     61                         {queryResults.hits.map((result, index) => (
     62                             <View
     63                                 wrap={result.content.body?.length > 500}
     64                                 key={index}
     65                                 style={{
     66                                     flexDirection: 'column',
     67                                     margin: 8,
     68                                     padding: 12,
     69                                     backgroundColor: '#e6e6e6',
     70                                     gap: 16,
     71                                     borderTopLeftRadius: borderRadius,
     72                                     borderTopRightRadius: borderRadius,
     73                                     borderBottomLeftRadius: borderRadius,
     74                                     borderBottomRightRadius: borderRadius,
     75                                     borderStyle: 'solid',
     76                                     borderWidth: 1,
     77                                     borderColor: '#00000'
     78                                 }}>
     79                                 <View
     80                                     style={{ flexDirection: 'row', justifyContent: 'space-between', fontSize: 10 }}>
     81                                     <Text>{result.room_name ?? result.room_id}</Text>
     82                                     <View style={{ flexDirection: 'row' }}>
     83                                         <Text>{result.sender}</Text>
     84                                         <Link style={{ marginLeft: 5 }} src={`https://matrix.to/#/${result.room_id}/${result.id}`}>Link</Link>
     85                                     </View>
     86                                 </View>
     87                                 <Text>{result.content.body}</Text>
     88                             </View>
     89                         ))}
     90                     </View>
     91                     <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10, fontSize: 10 }} fixed>
     92                         <Text render={({ pageNumber, totalPages }) => (
     93                             `${pageNumber} / ${totalPages}`
     94                         )} />
     95                         <Text>
     96                             Matrix Search Engine
     97                         </Text>
     98                     </View>
     99                 </View>
    100             </Page>
    101         </Document >
    102     )
    103 }
    104 
    105 export async function renderPDFToDisk(query: string, room_id?: string, sender?: string, includeRedactions: boolean = false, includeEdits: boolean = false) {
    106     let cleanedRoomId = room_id?.trim();
    107     let cleanedSender = sender?.trim();
    108     if (cleanedRoomId === "") {
    109         cleanedRoomId = undefined;
    110     }
    111     if (cleanedSender === "") {
    112         cleanedSender = undefined;
    113     }
    114     const queryResults = await indexer.search(query, cleanedRoomId, cleanedSender);
    115 
    116     // Filter out redactions if the user does not want them
    117     if (!includeRedactions) {
    118         queryResults.hits = queryResults.hits.filter((hit) => !hit.redacted);
    119     }
    120     // Filter out edits if the user does not want them
    121     if (!includeEdits) {
    122         queryResults.hits = queryResults.hits.filter((hit) => !hit.edited);
    123     }
    124 
    125     //console.log(`Search results:`, queryResults);
    126     await ReactPDF.renderToFile(<MessageDocument query={query} room_id={cleanedRoomId} sender={cleanedSender} queryResults={queryResults} />, `output.pdf`);
    127 }
    128 
    129 export async function renderPDFToBufferWithData(queryResults: { hits: Hits<Record<string, any>>; estimatedTotalHits: number; }, query?: string, room_id?: string, sender?: string,): Promise<Buffer> {
    130     return await renderToBuffer(<MessageDocument query={query ?? ""} room_id={room_id} sender={sender} queryResults={queryResults} />);
    131 }
    132 
    133 
    134 const pathToThisFile = resolve(fileURLToPath(import.meta.url))
    135 const pathPassedToNode = resolve(process.argv[1])
    136 const isThisFileBeingRunViaCLI = pathToThisFile.includes(pathPassedToNode)
    137 
    138 if (isThisFileBeingRunViaCLI) {
    139     const questions: PromptObject<string>[] = [
    140         {
    141             type: 'text',
    142             name: 'query',
    143             initial: "",
    144             message: 'Enter your search query:',
    145         },
    146         {
    147             type: 'text',
    148             name: 'room_id',
    149             initial: undefined,
    150             message: 'Enter the Room ID:',
    151         },
    152         {
    153             type: 'text',
    154             name: 'sender',
    155             initial: undefined,
    156             message: 'Enter the sender:',
    157         },
    158         // Ask if the redactions should be included
    159         {
    160             type: 'confirm',
    161             name: 'include_redactions',
    162             initial: false,
    163             message: 'Include redactions in the search results?',
    164         },
    165         // Ask if the edits should be included
    166         {
    167             type: 'confirm',
    168             name: 'include_edits',
    169             initial: false,
    170             message: 'Include edits in the search results?',
    171         }
    172     ]
    173 
    174     const response = await prompts(questions);
    175 
    176     await renderPDFToDisk(response.query as string, response.room_id as string | undefined, response.sender as string | undefined, response.include_redactions as boolean, response.include_edits as boolean);
    177     console.log("PDF rendered to disk");
    178 }