redirecter for ao3 that adds opengraph metadata
1import DOM from "fauxdom"
2import { readFile } from 'node:fs/promises'
3import querystring from 'node:querystring'
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 getWork = async (workId, archive = null) => {
10 const domainParam = (archive && archive !== process.env.ARCHIVE) ? `?archive=${archive}` : ''
11 const data = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
12 const work = await data.json()
13 return work
14}
15
16const getHighestRating = async (works, archive = null) => {
17 const ratings = await Promise.all(works.map(async (w) => {
18 const work = await getWork(w.id, archive)
19 return work.rating
20 }))
21 if (ratings.includes("Not Rated")) {
22 return "NR"
23 } else if (ratings.includes("Explicit")) {
24 return "E"
25 } else if (ratings.includes("Mature")) {
26 return "M"
27 } else if (ratings.includes("Teen")) {
28 return "T"
29 }
30 return "G"
31}
32
33const getHighestWarning = async (works, archive = null) => {
34 const warnings = await Promise.all(works.map(async (w) => {
35 const work = await getWork(w.id, archive)
36 return work.tags.warnings
37 }))
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, archive = null) => {
48 const categories = await Promise.all(works.map(async (w) => {
49 const work = await getWork(w.id, archive)
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 archive = props && props.archive ? props.archive : process.env.ARCHIVE
86 const baseFont = props.baseFont ? props.baseFont : process.env.DEFAULT_BASE_FONT
87 const baseFontData = baseFonts[baseFont]
88 const titleFont = props.titleFont ? props.titleFont : process.env.DEFAULT_TITLE_FONT
89 const titleFontData = titleFonts[titleFont]
90 const themeData = props.theme ? themes[props.theme] : themes[process.env.DEFAULT_THEME]
91 const parentWork = type === 'work' && data.chapterInfo ? await getWork(data.id, archive) : 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' ? data.rating : await getHighestRating(data.works, archive)
120 const warning = type === 'work' ? await getHighestWarning([data], archive) : await getHighestWarning(data.works, archive)
121 const category = type === 'work' ? await getCategory([data], archive) : await getCategory(data.works, archive)
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 ? (props.summaryType === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.summaryType === 'custom' && props.customSummary !== '' ? props.customSummary : (data.summary ? data.summary : (parentWork ? parentWork.summary : ''))))
128 : (props.summaryType === 'custom' && props.customSummary !== '' ? props.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 const ret = {
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: props,
183 opts: {
184 fonts: bfs.concat(tfs)
185 }
186 }
187 return ret;
188}