import 'dotenv/config' import { JSDOM } from 'jsdom' import sanitizeHtml from 'sanitize-html' export function markdownLinkRegex(url) { return new RegExp( `(\\[).*?(\\]\\(https:\\/\\/${url.replaceAll('.', '\\.')}\\))`, 'i', ) } export function htmlLinkRegex(url) { const escapedUrl = url.replaceAll('.', '\\.') return new RegExp( `(]+href=["']https?:\\/\\/${escapedUrl}[^"']*["'][^>]*>).*?(<\\/a>)`, 'i', ) } export function markdownCharEscape(text) { return text .replaceAll('`', '\\`') .replaceAll('*', '\\*') .replaceAll('_', '\\_') .replaceAll('{', '\\{') .replaceAll('}', '\\}') .replaceAll('[', '\\[') .replaceAll(']', '\\]') .replaceAll('(', '\\(') .replaceAll(')', '\\)') .replaceAll('#', '\\#') .replaceAll('+', '\\+') .replaceAll('-', '\\-') .replaceAll('.', '\\.') .replaceAll('!', '\\!') } export async function getRSSItemTitle(feedUrl) { const res = await fetch(feedUrl) const data = await res.text() const dom = new JSDOM(data, { contentType: 'text/xml' }) const item = dom.window.document.querySelector('item') const title = item.querySelector('title').textContent const link = item.querySelector('link').textContent.trim() const cleanTitle = sanitizeHtml(title, { allowedTags: [], allowedAttributes: {}, })?.trim() return { text: cleanTitle, url: link } } export async function getMastodonPost(feedUrl) { const res = await fetch(feedUrl) const data = await res.text() const dom = new JSDOM(data, { contentType: 'text/xml' }) // get all items from the rss feed const items = dom.window.document.querySelectorAll('item') // find the first item that isn't a reply (doesn't start with @) for (const item of items) { const description = item.querySelector('description')?.textContent || '' // skip if it's a reply (starts with @ mention) if (description.trim().startsWith('@')) { continue } const link = item.querySelector('link')?.textContent.trim() // use description as the text, stripping HTML let cleanText = sanitizeHtml(description, { allowedTags: [], allowedAttributes: {}, })?.trim() // replace URLs with link emoji cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() return { text: cleanText, url: link } } // fallback to first item if no non-reply found const firstItem = items[0] const description = firstItem.querySelector('description')?.textContent || '' const link = firstItem.querySelector('link')?.textContent.trim() let cleanText = sanitizeHtml(description, { allowedTags: [], allowedAttributes: {}, })?.trim() // replace URLs with link emoji cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() return { text: cleanText, url: link } } export async function getJsonFeedItemTitle(feedUrl, showImage = true) { const res = await fetch(feedUrl) const data = await res.json() const post = data.items[0] const { title, image, url } = post const text = !showImage ? title : image ? `${title} ![${title}](${image})` : title return { text, url } } export async function getJsonFeedItemContent(feedUrl, showImage = true) { const res = await fetch(feedUrl) const data = await res.json() const post = data.items[0] const { content_html, image, url } = post const title = sanitizeHtml(content_html, { allowedTags: [], allowedAttributes: {}, })?.trim() const text = !showImage ? title : image ? `${title} ![${title}](${image})` : title return { text, url } } export async function getMalojaScrobble(malojaUrl) { const res = await fetch(`${malojaUrl}/apis/mlj_1/scrobbles?perpage=1`) const data = await res.json() const scrobble = data.list[0] const { title, album: { artists }, } = scrobble.track return `${title}
by ${artists.join(', ')}` } export async function getLetterboxdActivity(username, showImage = true) { const res = await fetch(`https://letterboxd.com/${username}/rss/`) const data = await res.text() const dom = new JSDOM(data, { contentType: 'text/xml' }) const item = dom.window.document.querySelector('item') const title = item.querySelector('title').textContent const link = item.querySelector('link').textContent.trim() let text = title if (showImage) { // Parse CDATA content as HTML to extract image const description = item.querySelector('description').textContent const imgMatch = description.match(/]+src="([^"]+)"/) const image = imgMatch ? imgMatch[1] : null text = image ? `${title} ![${title}](${image})` : title } return { text, url: link } } export async function getTraktEpisode(username, id, showImage = true) { const res = await fetch( `https://trakt.tv/users/${username}/history/episodes/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, ) const data = await res.text() const dom = new JSDOM(data) const entry = dom.window.document.querySelector('entry') const title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') const text = !showImage ? title : `${title} ![${title}](https://widgets.trakt.tv/users/${id}/watched/thumb@2x.jpg?type=episode&image_only=1)` return { text, url: link } } export async function getTraktMovie(username, id, showImage = true) { const res = await fetch( `https://trakt.tv/users/${username}/history/movies/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, ) const data = await res.text() const dom = new JSDOM(data) const entry = dom.window.document.querySelector('entry') const title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') const text = !showImage ? title : `${title} ![${title}](https://widgets.trakt.tv/users/${id}/watched/thumb@2x.jpg?type=movie&image_only=1)` return { text, url: link } } export async function getTraktEpisodeAndMovie(username, id, showImage = true) { const episode = await getTraktEpisode(username, id, showImage) const movie = await getTraktMovie(username, id, showImage) return { text: `${episode.text}
and
${movie.text}`, url: `https://trakt.tv/users/${username}`, } } export async function getReadingBooklogr(booklogrUrl, booklogrUser) { const res = await fetch(`${booklogrUrl}/v1/profiles/${booklogrUser}`) const data = await res.json() const books = data.books.filter( ({ reading_status }) => reading_status === 'Currently reading', ) return books.map(({ title }) => title).join(',
') }