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