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}