1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-22 16:17:29 +00:00

Rewrite feeds

This commit is contained in:
Cadence Fish 2020-02-18 13:39:20 +13:00
parent b10432aa38
commit 5201a6612b
No known key found for this signature in database
GPG Key ID: 81015DF9AA8607E1
16 changed files with 118 additions and 65 deletions

View File

@ -77,7 +77,9 @@ See [Wiki:Installing](https://github.com/cloudrac3r/bibliogram/wiki/Installing)
- `/` - homepage - `/` - homepage
- `/u/{username}` - load a user's profile and timeline - `/u/{username}` - load a user's profile and timeline
- `/u/{username}/rss.xml` - get the RSS feed for a user - `/u/{username}/rss.xml` - get the RSS feed for a user
- `/u/{username}/atom.xml` - get the Atom feed for a user
- `/p/{shortcode}` - load a post - `/p/{shortcode}` - load a post
- `/privacy` - privacy policy
## Credits ## Credits

47
package-lock.json generated
View File

@ -1096,6 +1096,13 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
}, },
"feed": {
"version": "github:cloudrac3r/feed#dbd55889e9c7135a8710eaa4d4c415ffeee7fc27",
"from": "github:cloudrac3r/feed#dbd55889e9c7135a8710eaa4d4c415ffeee7fc27",
"requires": {
"xml-js": "^1.6.11"
}
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -3126,30 +3133,6 @@
"glob": "^7.1.3" "glob": "^7.1.3"
} }
}, },
"rss": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
"integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=",
"requires": {
"mime-types": "2.1.13",
"xml": "1.0.1"
},
"dependencies": {
"mime-db": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
"integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I="
},
"mime-types": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
"integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=",
"requires": {
"mime-db": "~1.25.0"
}
}
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -3208,6 +3191,11 @@
} }
} }
}, },
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scss-tokenizer": { "scss-tokenizer": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
@ -5023,10 +5011,13 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
"integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==" "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
}, },
"xml": { "xml-js": {
"version": "1.0.1", "version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
}, },
"y18n": { "y18n": {
"version": "3.2.1", "version": "3.2.1",

View File

@ -12,12 +12,12 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"better-sqlite3": "^5.4.3", "better-sqlite3": "^5.4.3",
"feed": "github:cloudrac3r/feed#dbd55889e9c7135a8710eaa4d4c415ffeee7fc27",
"mixin-deep": "^2.0.1", "mixin-deep": "^2.0.1",
"node-dir": "^0.1.17", "node-dir": "^0.1.17",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"pinski": "github:cloudrac3r/pinski#9eb56d90fdd00357451dd5a546dbcca1f9bf114a", "pinski": "github:cloudrac3r/pinski#9eb56d90fdd00357451dd5a546dbcca1f9bf114a",
"pug": "^2.0.4", "pug": "^2.0.4",
"rss": "^1.2.2",
"semver": "^7.1.2", "semver": "^7.1.2",
"sharp": "^0.24.0", "sharp": "^0.24.0",
"socks-proxy-agent": "github:cloudrac3r/node-socks-proxy-agent#6a26d274b12098dfef6cc2faafd25b0c051f2467" "socks-proxy-agent": "github:cloudrac3r/node-socks-proxy-agent#6a26d274b12098dfef6cc2faafd25b0c051f2467"

View File

@ -13,6 +13,10 @@ const userRequestCache = new UserRequestCache(constants.caching.resource_cache_t
const timelineEntryCache = new TtlCache(constants.caching.resource_cache_time) const timelineEntryCache = new TtlCache(constants.caching.resource_cache_time)
const history = new RequestHistory(["user", "timeline", "post", "reel"]) const history = new RequestHistory(["user", "timeline", "post", "reel"])
/**
* @param {string} username
* @param {boolean} isRSS
*/
async function fetchUser(username, isRSS) { async function fetchUser(username, isRSS) {
let mode = constants.allow_user_from_reel let mode = constants.allow_user_from_reel
if (mode === "preferForRSS") { if (mode === "preferForRSS") {
@ -38,6 +42,10 @@ async function fetchUser(username, isRSS) {
} }
} }
/**
* @param {string} username
* @returns {Promise<import("./structures/User")>}
*/
function fetchUserFromHTML(username) { function fetchUserFromHTML(username) {
return userRequestCache.getOrFetch("user/"+username, false, true, () => { return userRequestCache.getOrFetch("user/"+username, false, true, () => {
return switcher.request("user_html", `https://www.instagram.com/${username}/`, async res => { return switcher.request("user_html", `https://www.instagram.com/${username}/`, async res => {
@ -72,6 +80,11 @@ function fetchUserFromHTML(username) {
}) })
} }
/**
* @param {string} userID
* @param {string} username
* @returns {Promise<import("./structures/ReelUser")>}
*/
function fetchUserFromCombined(userID, username) { function fetchUserFromCombined(userID, username) {
// Fetch basic user information // Fetch basic user information
const p = new URLSearchParams() const p = new URLSearchParams()

View File

@ -6,7 +6,7 @@
let constants = { let constants = {
// Things that server owners _should_ change! // Things that server owners _should_ change!
website_origin: "http://localhost:10407", website_origin: "http://localhost:10407", // Protocol and domain that this instance is hosted on. Do NOT include a trailing slash.
has_privacy_policy: false, // You MUST read /src/site/pug/privacy.pug.template before changing this! has_privacy_policy: false, // You MUST read /src/site/pug/privacy.pug.template before changing this!
// Things that server owners _could_ change if they want to. // Things that server owners _could_ change if they want to.

View File

@ -13,11 +13,16 @@ class ReelUser {
this.following = 0 this.following = 0
this.followedBy = 0 this.followedBy = 0
this.posts = 0 this.posts = 0
/** @type {import("./Timeline")} */
this.timeline = new Timeline(this) this.timeline = new Timeline(this)
this.cachedAt = Date.now() this.cachedAt = Date.now()
this.proxyProfilePicture = proxyImage(this.data.profile_pic_url) this.proxyProfilePicture = proxyImage(this.data.profile_pic_url)
} }
getStructuredBio() {
return null
}
getTtl(scale = 1) { getTtl(scale = 1) {
const expiresAt = this.cachedAt + constants.caching.resource_cache_time const expiresAt = this.cachedAt + constants.caching.resource_cache_time
const ttl = expiresAt - Date.now() const ttl = expiresAt - Date.now()

View File

@ -1,4 +1,4 @@
const RSS = require("rss") const {Feed} = require("feed")
const constants = require("../constants") const constants = require("../constants")
const config = require("../../../config") const config = require("../../../config")
const TimelineEntry = require("./TimelineEntry") const TimelineEntry = require("./TimelineEntry")
@ -54,18 +54,27 @@ class Timeline {
} }
async fetchFeed() { async fetchFeed() {
const feed = new RSS({ // we likely cannot use full_name here - reel fallback would make the feed title inconsistent, leading to confusing experience
title: `@${this.user.data.username}`, const usedName = `@${this.user.data.username}`
feed_url: `${config.website_origin}/u/${this.user.data.username}/rss.xml`, const feed = new Feed({
site_url: config.website_origin, title: usedName,
description: this.user.data.biography, description: this.user.data.biography,
image_url: config.website_origin+this.user.proxyProfilePicture, id: `bibliogram:user/${this.user.data.username}`,
pubDate: new Date(this.user.cachedAt), link: `${constants.website_origin}/u/${this.user.data.username}`,
ttl: this.user.getTtl(1000*60) // scale to minute feedLinks: {
rss: `${constants.website_origin}/u/${this.user.data.username}/rss.xml`,
atom: `${constants.website_origin}/u/${this.user.data.username}/atom.xml`
},
image: constants.website_origin+this.user.proxyProfilePicture,
updated: new Date(this.user.cachedAt),
author: {
name: usedName,
link: `${constants.website_origin}/u/${this.user.data.username}`
}
}) })
const page = this.pages[0] // only get posts from first page const page = this.pages[0] // only get posts from first page
await Promise.all(page.map(item => await Promise.all(page.map(item =>
item.fetchFeedData().then(feedData => feed.item(feedData)) item.fetchFeedData().then(feedData => feed.addItem(feedData))
)) ))
return feed return feed
} }

View File

@ -217,6 +217,9 @@ class TimelineEntry extends TimelineBaseMethods {
else return this.update().then(() => this.getVideoUrlP()) else return this.update().then(() => this.getVideoUrlP())
} }
/**
* @returns {Promise<import("feed/src/typings/index").Item>}
*/
async fetchFeedData() { async fetchFeedData() {
const children = await this.fetchChildren() const children = await this.fetchChildren()
return { return {
@ -230,10 +233,10 @@ class TimelineEntry extends TimelineBaseMethods {
height: child.data.dimensions.height height: child.data.dimensions.height
})) }))
}), }),
author: this.data.owner.username, link: `${constants.website_origin}/p/${this.data.shortcode}`,
url: `${constants.website_origin}/p/${this.data.shortcode}`, id: `bibliogram:post/${this.data.shortcode}`, // Is it wise to keep the origin in here? The same post would have a different ID from different servers.
guid: `${constants.website_origin}/p/${this.data.shortcode}`, // Is it wise to keep the origin in here? The same post would have a different ID from different servers. published: new Date(this.data.taken_at_timestamp*1000), // first published date
date: new Date(this.data.taken_at_timestamp*1000) date: new Date(this.data.taken_at_timestamp*1000) // last modified date
/* /*
Readers should display the description as HTML rather than using the media enclosure. Readers should display the description as HTML rather than using the media enclosure.
enclosure: { enclosure: {

View File

@ -3,7 +3,7 @@ const fetch = require("node-fetch").default
function request(url, options = {}, settings = {}) { function request(url, options = {}, settings = {}) {
if (settings.statusLine === undefined) settings.statusLine = "OUT" if (settings.statusLine === undefined) settings.statusLine = "OUT"
if (settings.log === undefined) settings.log = true if (settings.log === undefined) settings.log = true
if (settings.log) console.log(`-> [${settings.statusLine}] ${url}`) // todo: make more like pinski? if (settings.log) console.log(` -> [${settings.statusLine}] ${url}`) // todo: make more like pinski?
// @ts-ignore // @ts-ignore
return fetch(url, Object.assign({ return fetch(url, Object.assign({
headers: { headers: {

View File

@ -4,15 +4,29 @@ const {render} = require("pinski/plugins")
const {pugCache} = require("../passthrough") const {pugCache} = require("../passthrough")
module.exports = [ module.exports = [
{route: `/u/(${constants.external.username_regex})/rss.xml`, methods: ["GET"], code: ({fill}) => { {route: `/u/(${constants.external.username_regex})/(rss|atom)\\.xml`, methods: ["GET"], code: ({fill}) => {
if (constants.settings.rss_enabled) { if (constants.settings.rss_enabled) {
const kind = fill[1]
return fetchUser(fill[0], true).then(async user => { return fetchUser(fill[0], true).then(async user => {
const content = await user.timeline.fetchFeed() const feed = await user.timeline.fetchFeed()
const xml = content.xml() if (kind === "rss") {
var data = {
contentType: "application/rss+xml", // see https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed,
content: feed.rss2()
}
} else if (kind === "atom") {
var data = {
contentType: "application/atom+xml", // see https://en.wikipedia.org/wiki/Atom_(standard)#Including_in_HTML
content: feed.atom1()
}
}
return { return {
statusCode: 200, statusCode: 200,
contentType: "application/rss+xml", // see https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed contentType: data.contentType,
content: xml headers: {
"Cache-Control": `max-age=${userRequestCache.getTtl("user/"+user.data.username, 1000)}`
},
content: data.content
} }
}).catch(error => { }).catch(error => {
if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) { if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) {
@ -40,8 +54,8 @@ module.exports = [
} else { } else {
return Promise.resolve(render(403, "pug/friendlyerror.pug", { return Promise.resolve(render(403, "pug/friendlyerror.pug", {
statusCode: 403, statusCode: 403,
title: "RSS disabled", title: "Feeds disabled",
message: "RSS is disabled on this instance.", message: "Feeds are disabled on this instance.",
withInstancesLink: true withInstancesLink: true
})) }))
} }

View File

@ -58,7 +58,7 @@ module.exports = [
if (typeof page === "number" && !isNaN(page) && page >= 1) { if (typeof page === "number" && !isNaN(page) && page >= 1) {
await user.timeline.fetchUpToPage(page - 1) await user.timeline.fetchUpToPage(page - 1)
} }
return render(200, "pug/user.pug", {url, user, constants}) return render(200, "pug/user.pug", {url, user, constants, website_origin: constants.website_origin})
}).catch(error => { }).catch(error => {
if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) { if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) {
return render(404, "pug/friendlyerror.pug", { return render(404, "pug/friendlyerror.pug", {

View File

@ -8,7 +8,7 @@ html
body.homepage body.homepage
header header
h1.banner h1.banner
img.banner-image(src="/static/img/banner-min.svg") img.banner-image(src="/static/img/banner-min.svg" alt="Bibliogram")
.go-sections-container .go-sections-container
.go-sections .go-sections
section section

View File

@ -0,0 +1,9 @@
mixin feed_link(name, urlPart, username, contentType)
span
a(rel="alternate" type=contentType href=`/u/${username}/${urlPart}.xml`)
= name
sup.validate-feed
-
let params = new URLSearchParams()
params.set("url", `${website_origin}/u/${username}/${urlPart}.xml`)
a(href="https://validator.w3.org/feed/check.cgi?"+params.toString() title="Validate this feed") v!

View File

@ -1,8 +1,9 @@
//- Needs user, url, constants //- Needs user, url, constants, website_origin
include includes/timeline_page.pug include includes/timeline_page.pug
include includes/next_page_button.pug include includes/next_page_button.pug
include includes/display_structured include includes/display_structured
include includes/feed_link
- const numberFormat = new Intl.NumberFormat().format - const numberFormat = new Intl.NumberFormat().format
@ -19,7 +20,7 @@ html
.main-divider .main-divider
header.profile-overview header.profile-overview
.profile-sticky .profile-sticky
img(src=user.proxyProfilePicture width="150px" height="150px" alt=`${user.data.full_name || user.data.username}'s profile picture.`).pfp img(src=user.proxyProfilePicture width=150 height=150 alt=`${user.data.full_name || user.data.username}'s profile picture.`).pfp
//- //-
Instagram only uses the above URL, but an HD version is also available. Instagram only uses the above URL, but an HD version is also available.
The alt text is pathetic, I know. I don't have much to work with. The alt text is pathetic, I know. I don't have much to work with.
@ -28,20 +29,23 @@ html
h2.username= `@${user.data.username}` h2.username= `@${user.data.username}`
else else
h1.full-name= `@${user.data.username}` h1.full-name= `@${user.data.username}`
if !user.fromReel p.structured-text.bio
p.structured-text.bio - const bio = user.getStructuredBio()
- const bio = user.getStructuredBio() if bio
if bio +display_structured(bio)
+display_structured(bio) if user.data.external_url
if user.data.external_url p.website
p.website a(href=user.data.external_url)= user.data.external_url
a(href=user.data.external_url)= user.data.external_url if user.posts != undefined
div.profile-counter #[span(data-numberformat=user.posts).count #{numberFormat(user.posts)}] posts div.profile-counter #[span(data-numberformat=user.posts).count #{numberFormat(user.posts)}] posts
if user.following != undefined
div.profile-counter #[span(data-numberformat=user.following).count #{numberFormat(user.following)}] following div.profile-counter #[span(data-numberformat=user.following).count #{numberFormat(user.following)}] following
if user.followedBy != undefined
div.profile-counter #[span(data-numberformat=user.followedBy).count #{numberFormat(user.followedBy)}] followed by div.profile-counter #[span(data-numberformat=user.followedBy).count #{numberFormat(user.followedBy)}] followed by
div.links div.links
if constants.settings.rss_enabled if constants.settings.rss_enabled
a(rel="alternate" type="application/rss+xml" href=`/u/${user.data.username}/rss.xml`) RSS +feed_link("RSS", "rss", user.data.username, "application/rss+xml")
+feed_link("Atom", "atom", user.data.username, "application/atom+xml")
a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}`) instagram.com a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}`) instagram.com
- const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length - const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length

View File

@ -102,6 +102,9 @@ body
flex-wrap: wrap flex-wrap: wrap
justify-content: center justify-content: center
.validate-feed
margin-left: 2px
a, a:visited a, a:visited
color: $main-theme-link-color color: $main-theme-link-color

View File

@ -21,7 +21,6 @@ subdirs("pug", async (err, dirs) => {
pinski.addPugDir("pug", dirs) pinski.addPugDir("pug", dirs)
pinski.addAPIDir("html/static/js/templates/api") pinski.addAPIDir("html/static/js/templates/api")
pinski.addSassDir("sass") pinski.addSassDir("sass")
pinski.addAPIDir("api")
pinski.muteLogsStartingWith("/imageproxy") pinski.muteLogsStartingWith("/imageproxy")
pinski.muteLogsStartingWith("/videoproxy") pinski.muteLogsStartingWith("/videoproxy")
pinski.muteLogsStartingWith("/static") pinski.muteLogsStartingWith("/static")
@ -30,6 +29,7 @@ subdirs("pug", async (err, dirs) => {
await require("../lib/utils/tor") // make sure tor state is known before going further await require("../lib/utils/tor") // make sure tor state is known before going further
} }
pinski.addAPIDir("api")
pinski.startServer() pinski.startServer()
pinski.enableWS() pinski.enableWS()