redirecter for ao3 that adds opengraph metadata
1import { getWork } from "@fujocoded/ao3.js" 2import DOM from "fauxdom" 3import { readFile } from 'node:fs/promises' 4import { join } from 'node:path' 5import themes from '@/lib/themes.js' 6import baseFonts from '@/lib/baseFonts.js' 7import titleFonts from '@/lib/titleFonts.js' 8 9const getHighestRating = async (works) => { 10 const ratings = await Promise.all(works.map(async (w) => { 11 const work = await getWork({workId: w.id}) 12 return work.rating 13 })) 14 if (ratings.includes("Not Rated")) { 15 return "NR" 16 } else if (ratings.includes("Explicit")) { 17 return "E" 18 } else if (ratings.includes("Mature")) { 19 return "M" 20 } else if (ratings.includes("Teen")) { 21 return "T" 22 } 23 return "G" 24} 25 26const getHighestWarning = async (works) => { 27 const warnings = await Promise.all(works.map(async (w) => { 28 const work = await getWork({workId: w.id}) 29 return work.tags.warnings 30 })) 31 const warningsUnique = warnings.reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === warnings.indexOf(w) }) 32 if (warningsUnique.length === 1 && warningsUnique[0] === "Creator Chose Not To Use Archive Warnings") { 33 return "CNTW" 34 } else if (warningsUnique.length === 1 && warningsUnique[0] === "No Archive Warnings Apply") { 35 return "NW" 36 } 37 return "W" 38} 39 40const getCategory = async (works) => { 41 const categories = await Promise.all(works.map(async (w) => { 42 const work = await getWork({workId: w.id}) 43 return work.category 44 })) 45 const categoriesJoined = categories.reduce((a, b) => { return a.concat(b) }) 46 const categoriesUnique = categoriesJoined.filter((w, i) => { return i === categoriesJoined.indexOf(w) }) 47 48 if (categoriesUnique.length === 1) { 49 if (categoriesUnique[0] === "F/F") return "F" 50 if (categoriesUnique[0] === "M/M") return "M" 51 if (categoriesUnique[0] === "F/M") return "FM" 52 if (categoriesUnique[0] === "Gen") return "G" 53 if (categoriesUnique[0] === "Multi") return "MX" 54 if (categoriesUnique[0] === "Other") return "O" 55 } 56 return "MX" 57} 58 59const sanitizeProps = (props) => { 60 let propsParsed = {} 61 Object.keys(props).forEach((pr) => { 62 if (props[pr] === 'true') { 63 propsParsed[pr] = true 64 return 65 } else if (props[pr] === 'false') { 66 propsParsed[pr] = false 67 return 68 } else if (typeof parseInt(props[pr]) === 'Number') { 69 propsParsed[pr] = parseInt(props[pr]) 70 return 71 } 72 propsParsed[pr] = props[pr] 73 }) 74 return propsParsed 75} 76 77export default async function sanitizeData ({ type, data, props}) { 78 const propsParsed = sanitizeProps(props) 79 const baseFont = propsParsed.baseFont ? propsParsed.baseFont : process.env.DEFAULT_BASE_FONT 80 const baseFontData = baseFonts[baseFont] 81 const titleFont = propsParsed.titleFont ? propsParsed.titleFont : process.env.DEFAULT_TITLE_FONT 82 const titleFontData = titleFonts[titleFont] 83 const themeData = propsParsed.theme ? themes[propsParsed.theme] : themes[process.env.DEFAULT_THEME] 84 const parentWork = type === 'work' && data.chapterInfo ? await getWork({workId: data.id}) : null 85 const bfs = await Promise.all(baseFontData.defs.map(async (bf) => { 86 return { 87 name: baseFontData.displayName, 88 data: await readFile( 89 join(process.cwd(), bf.path) 90 ), 91 style: bf.style, 92 weight: bf.weight 93 } 94 })).then(x => x) 95 const tfs = await Promise.all(titleFontData.defs.map(async (tf) => { 96 return { 97 name: titleFontData.displayName, 98 data: await readFile( 99 join(process.cwd(), tf.path) 100 ), 101 style: tf.style, 102 weight: tf.weight 103 } 104 })).then(x => x) 105 const authorsFormatted = data.authors 106 ? data.authors.map((a) => { 107 if (a.anonymous) return "Anonymous" 108 if (a.pseud !== a.username) return `${a.pseud} (${a.username})` 109 return a.username 110 }) 111 : [] 112 const rating = type === 'work' ? await getHighestRating([data]) : await getHighestRating(data.works) 113 const warning = type === 'work' ? await getHighestWarning([data]) : await getHighestWarning(data.works) 114 const category = type === 'work' ? await getCategory([data]) : await getCategory(data.works) 115 const authorString = authorsFormatted.length > 1 116 ? authorsFormatted.slice(0, -1).join(", ") + " & " + 117 authorsFormatted.slice(-1)[0] 118 : authorsFormatted[0] 119 const summaryContent = type === 'work' 120 ? (propsParsed.summaryType === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (propsParsed.summaryType === 'custom' && propsParsed.customSummary !== '' ? propsParsed.customSummary : (data.summary ? data.summary : (parentWork ? parentWork.summary : '')))) 121 : (propsParsed.summaryType === 'custom' && propsParsed.customSummary !== '' ? propsParsed.customSummary : data.notes) 122 const formatter = new Intl.NumberFormat('en-US') 123 const words = formatter.format(data.words) 124 const summaryDOM = new DOM(summaryContent, {decodeEntities: true}); 125 const summaryFormatted = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace( 126 /(<([^>]+)>)/ig, 127 "", 128 ).split("\n") 129 const titleString = type === 'work' ? data.title : data.name 130 const chapterString = data.chapterInfo ? (data.chapterInfo.name 131 ? data.chapterInfo.name 132 : "Chapter " + data.chapterInfo.index) : null 133 const chapterCountString = type === 'work' ? (data.chapters 134 ? data.chapters.published+'/'+( 135 data.chapters.total 136 ? data.chapters.total 137 : '?' 138 ) 139 : '') : null 140 const fandomString = type === 'work' ? ( 141 data.fandoms.length > 1 142 ? ( 143 data.fandoms.length <= 2 144 ? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1) 145 : data.fandoms.join(", ")+" (+"+(data.fandoms.length - 2)+")" 146 ) 147 : data.fandoms[0] 148 ) : ( 149 '' 150 ) 151 const charTags = type === 'work' ? data.tags.characters : data.works.map(w => w.tags.characters).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) }) 152 const relTags = type === 'work' ? data.tags.relationships : data.works.map(w => w.tags.relationships).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) }) 153 const freeTags = type === 'work' ? data.tags.additional : data.works.map(w => w.tags.additional).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) }) 154 const warnings = type === 'work' ? data.tags.warnings : data.works.map(w => w.tags.warnings).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) }) 155 156 return { 157 topLine: fandomString, 158 titleLine: titleString, 159 authorLine: authorString, 160 chapterLine: chapterString, 161 chapterCount: chapterCountString, 162 words: words, 163 rating: rating, 164 warning: warning, 165 category: category, 166 summary: summaryFormatted, 167 theme: themeData, 168 charTags: charTags, 169 relTags: relTags, 170 freeTags: freeTags, 171 postedAt: type === 'work' ? data.publishedAt : data.startedAt, 172 updatedAt: data.updatedAt, 173 baseFont: baseFont, 174 titleFont: titleFont, 175 props: propsParsed, 176 opts: { 177 fonts: bfs.concat(tfs) 178 } 179 } 180}