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

Redesign disabled feed system

This commit is contained in:
Cadence Ember 2020-04-22 23:59:45 +12:00
parent 8300744d87
commit b7d3309a2b
No known key found for this signature in database
GPG Key ID: 128B99B1B74A6412
9 changed files with 210 additions and 89 deletions

View File

@ -39,9 +39,36 @@ let constants = {
allow_user_from_reel: "preferForRSS", // one of: "never", "fallback", "prefer", "onlyPreferSaved", "preferForRSS" allow_user_from_reel: "preferForRSS", // one of: "never", "fallback", "prefer", "onlyPreferSaved", "preferForRSS"
feeds: {
// Whether feeds are enabled.
enabled: true,
// Whether to display links to feeds on pages.
display_links: true,
// Whether to display the `v!` link to validate a feed.
display_validation_links: false,
// This feed message field allows you to insert a custom message into all RSS feeds to inform users of important changes,
// such as feeds being disabled forever on that instance.
feed_message: {
enabled: false,
// If the feed message is enabled, then `id` MUST be supplied.
// Please set it to `bibliogram:feed_announcement/your.domain/1`
// replacing `your.domain` with the address of your own domain,
// and incrementing `1` every time you make a new announcement (to make sure the IDs are unique).
id: "",
// The timestamp that you disabled feeds at. For example, if you disabled feeds forever starting at 2020-04-01T12:00:00 UTC,
// you should set this to 1585742400000.
timestamp: 0,
// The title of the feed item.
title: "Important message from Bibliogram",
// The text of the message.
message: "There is an important message about feeds on this Bibliogram instance. Please visit this link to read the message: ",
// The link address.
link: "https://your.domain/feedannouncement"
},
feed_disabled_max_age: 2*24*60*60 // 2 days
},
settings: { settings: {
rss_enabled: true,
display_feed_validation_buttons: false,
enable_updater_page: false enable_updater_page: false
}, },
@ -132,6 +159,8 @@ let constants = {
} }
}, },
additional_routes: [],
database_version: 3 database_version: 3
} }

View File

@ -4,6 +4,7 @@ const config = require("../../../config")
const TimelineEntry = require("./TimelineEntry") const TimelineEntry = require("./TimelineEntry")
const InstaCache = require("../cache") const InstaCache = require("../cache")
const collectors = require("../collectors") const collectors = require("../collectors")
const {getFeedSetup} = require("../utils/feed")
require("../testimports")(constants, collectors, TimelineEntry, InstaCache) require("../testimports")(constants, collectors, TimelineEntry, InstaCache)
/** @param {any[]} edges */ /** @param {any[]} edges */
@ -55,24 +56,8 @@ class Timeline {
} }
async fetchFeed() { async fetchFeed() {
// we likely cannot use full_name here - reel fallback would make the feed title inconsistent, leading to confusing experience const setup = getFeedSetup(this.user.data.username, this.user.data.biography, constants.website_origin+this.user.proxyProfilePicture, new Date(this.user.cachedAt))
const usedName = `@${this.user.data.username}` const feed = new Feed(setup)
const feed = new Feed({
title: usedName,
description: this.user.data.biography,
id: `bibliogram:user/${this.user.data.username}`,
link: `${constants.website_origin}/u/${this.user.data.username}`,
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.addItem(feedData)) item.fetchFeedData().then(feedData => feed.addItem(feedData))

View File

@ -245,7 +245,7 @@ class TimelineEntry extends TimelineBaseMethods {
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: {
url: this.data.display_url, url: this.data.display_url,
type: "image/jpeg" //TODO: can instagram have PNGs? everything is JPEG according to https://medium.com/@autolike.it/how-to-avoid-low-res-thumbnails-on-instagram-android-problem-bc24f0ed1c7d type: "image/jpeg" // Instagram only has JPEGs as far as I can tell
} }
*/ */
} }

23
src/lib/utils/feed.js Normal file
View File

@ -0,0 +1,23 @@
const constants = require("../constants")
function getFeedSetup(username, description, image, updated) {
const usedName = `@${username}`
return {
title: usedName,
description,
id: `bibliogram:user/${username}`,
link: `${constants.website_origin}/u/${username}`,
feedLinks: {
rss: `${constants.website_origin}/u/${username}/rss.xml`,
atom: `${constants.website_origin}/u/${username}/atom.xml`
},
image,
updated,
author: {
name: usedName,
link: `${constants.website_origin}/u/${username}`
}
}
}
module.exports.getFeedSetup = getFeedSetup

View File

@ -1,66 +1,109 @@
const constants = require("../../lib/constants") const constants = require("../../lib/constants")
const {Feed} = require("feed")
const {getFeedSetup} = require("../../lib/utils/feed")
const {fetchUser, userRequestCache} = require("../../lib/collectors") const {fetchUser, userRequestCache} = require("../../lib/collectors")
const {render} = require("pinski/plugins") const {render} = require("pinski/plugins")
const {pugCache} = require("../passthrough") const {pugCache} = require("../passthrough")
const {compile} = require("pug")
const rssAnnouncementTemplate = compile(`
p(style="white-space: pre-line") #{message}#[a(href=link)= link]
`)
function respondWithFeed(feed, kind, maxAge, available) {
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()
}
}
const headers = {
"Cache-Control": `public, max-age=${maxAge}`
}
if (!available) headers["X-Bibliogram-Feed-Unavailable"] = 1
return {
statusCode: 200, // must return 200 even if announcement only, since readers might not display anything with a failed status code
contentType: data.contentType,
headers,
content: data.content
}
}
/**
* @param {Feed} feed
*/
function addAnnouncementFeedItem(feed) {
feed.addItem({
title: constants.feeds.feed_message.title,
description: rssAnnouncementTemplate({
message: constants.feeds.feed_message.message,
link: constants.feeds.feed_message.link
}),
link: constants.feeds.feed_message.link,
id: constants.feeds.feed_message.id,
published: new Date(constants.feeds.feed_message.timestamp),
date: new Date(constants.feeds.feed_message.timestamp)
})
}
module.exports = [ module.exports = [
{route: `/u/(${constants.external.username_regex})/(rss|atom)\\.xml`, methods: ["GET"], code: ({fill}) => { {
if (constants.settings.rss_enabled) { route: `/u/(${constants.external.username_regex})/(rss|atom)\\.xml`, methods: ["GET"], code: ({fill}) => {
const kind = fill[1] const kind = fill[1]
return fetchUser(fill[0], constants.symbols.fetch_context.RSS).then(async user => { if (constants.feeds.enabled) {
const feed = await user.timeline.fetchFeed() return fetchUser(fill[0], constants.symbols.fetch_context.RSS).then(async user => {
if (kind === "rss") { const feed = await user.timeline.fetchFeed()
var data = { if (constants.feeds.feed_message.enabled) {
contentType: "application/rss+xml", // see https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed, addAnnouncementFeedItem(feed)
content: feed.rss2()
} }
} else if (kind === "atom") { return respondWithFeed(feed, kind, userRequestCache.getTtl("user/"+user.data.username, 1000), true)
var data = { }).catch(error => {
contentType: "application/atom+xml", // see https://en.wikipedia.org/wiki/Atom_(standard)#Including_in_HTML if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) {
content: feed.atom1() return render(404, "pug/friendlyerror.pug", {
} statusCode: 404,
} title: "Not found",
return { message: "This user doesn't exist.",
statusCode: 200, withInstancesLink: false
contentType: data.contentType,
headers: {
"Cache-Control": `public, max-age=${userRequestCache.getTtl("user/"+user.data.username, 1000)}`
},
content: data.content
}
}).catch(error => {
if (error === constants.symbols.NOT_FOUND || error === constants.symbols.ENDPOINT_OVERRIDDEN) {
return render(404, "pug/friendlyerror.pug", {
statusCode: 404,
title: "Not found",
message: "This user doesn't exist.",
withInstancesLink: false
})
} else if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN || error === constants.symbols.RATE_LIMITED) {
return {
statusCode: 503,
contentType: "text/html",
headers: {
"Cache-Control": `public, max-age=${userRequestCache.getTtl("user/"+fill[0], 1000)}`,
"Retry-After": userRequestCache.getTtl("user/"+fill[0], 1000)
},
content: pugCache.get("pug/blocked.pug").web({
expiresMinutes: userRequestCache.getTtl("user/"+fill[0], 1000*60)
}) })
} else if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN || error === constants.symbols.RATE_LIMITED) {
return {
statusCode: 503,
contentType: "text/html",
headers: {
"Cache-Control": `public, max-age=${userRequestCache.getTtl("user/"+fill[0], 1000)}`,
"Retry-After": userRequestCache.getTtl("user/"+fill[0], 1000)
},
content: pugCache.get("pug/blocked.pug").web({
expiresMinutes: userRequestCache.getTtl("user/"+fill[0], 1000*60)
})
}
} else if (error === constants.symbols.extractor_results.AGE_RESTRICTED) {
return render(403, "pug/age_gated.pug")
} else {
throw error
} }
} else if (error === constants.symbols.extractor_results.AGE_RESTRICTED) { })
return render(403, "pug/age_gated.pug") } else {
if (constants.feeds.feed_message.enabled) {
const setup = getFeedSetup(fill[0], "", undefined, new Date())
const feed = new Feed(setup)
addAnnouncementFeedItem(feed)
return Promise.resolve(respondWithFeed(feed, kind, constants.feeds.feed_disabled_max_age, false))
} else { } else {
throw error return Promise.resolve(render(403, "pug/friendlyerror.pug", {
statusCode: 403,
title: "Feeds disabled",
message: "Feeds are disabled on this instance.",
withInstancesLink: true
}))
} }
}) }
} else {
return Promise.resolve(render(403, "pug/friendlyerror.pug", {
statusCode: 403,
title: "Feeds disabled",
message: "Feeds are disabled on this instance.",
withInstancesLink: true
}))
} }
}} }
] ]

View File

@ -69,8 +69,7 @@ module.exports = [
await user.timeline.fetchUpToPage(page - 1) await user.timeline.fetchUpToPage(page - 1)
} }
const followerCountsAvailable = !(user.constructor.name === "ReelUser" && user.following === 0 && user.followedBy === 0) const followerCountsAvailable = !(user.constructor.name === "ReelUser" && user.following === 0 && user.followedBy === 0)
const {website_origin, settings: {display_feed_validation_buttons}} = constants return render(200, "pug/user.pug", {url, user, followerCountsAvailable, constants})
return render(200, "pug/user.pug", {url, user, followerCountsAvailable, constants, website_origin, display_feed_validation_buttons})
}).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

@ -1,4 +1,4 @@
//- Needs user, followerCountsAvailable, url, constants, website_origin, display_feed_validation_buttons //- Needs user, followerCountsAvailable, url, constants
include includes/timeline_page.pug include includes/timeline_page.pug
include includes/next_page_button.pug include includes/next_page_button.pug
@ -17,12 +17,12 @@ html
include includes/head include includes/head
script(src="/static/js/pagination.js" type="module") script(src="/static/js/pagination.js" type="module")
script(src="/static/js/post_overlay.js" type="module") script(src="/static/js/post_overlay.js" type="module")
meta(property="og:url" content=`${website_origin}/u/${user.data.username}`) meta(property="og:url" content=`${constants.website_origin}/u/${user.data.username}`)
meta(property="og:type" content="profile") meta(property="og:type" content="profile")
meta(property="og:title" content=(user.data.full_name || user.data.username)) meta(property="og:title" content=(user.data.full_name || user.data.username))
if user.data.biography if user.data.biography
meta(property="og:description" content=user.data.biography) meta(property="og:description" content=user.data.biography)
meta(property="og:image" content=`${website_origin}${user.proxyProfilePicture}`) meta(property="og:image" content=`${constants.website_origin}${user.proxyProfilePicture}`)
meta(property="og:image:width" content=150) meta(property="og:image:width" content=150)
meta(property="og:image:height" content=150) meta(property="og:image:height" content=150)
meta(property="og:image:type" content="image/jpeg") meta(property="og:image:type" content="image/jpeg")
@ -58,9 +58,9 @@ html
else else
div.profile-counter.not-available Followers not available. div.profile-counter.not-available Followers not available.
div.links div.links
if constants.settings.rss_enabled if constants.feeds.enabled && constants.feeds.display_links
+feed_link("RSS", "rss", user.data.username, "application/rss+xml", display_feed_validation_buttons) +feed_link("RSS", "rss", user.data.username, "application/rss+xml", constants.feeds.display_validation_links)
+feed_link("Atom", "atom", user.data.username, "application/atom+xml", display_feed_validation_buttons) +feed_link("Atom", "atom", user.data.username, "application/atom+xml", constants.feeds.display_validation_links)
a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}` target="_blank") instagram.com a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}` target="_blank") 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

@ -4,6 +4,7 @@ $layout-c-max: 680px
$layout-home-a-max: 520px $layout-home-a-max: 520px
$layout-home-b-min: 521px $layout-home-b-min: 521px
$main-theme-link-color: #085cae $main-theme-link-color: #085cae
$medium-red-bg: #6a2222
@font-face @font-face
font-family: "Bariol" font-family: "Bariol"
@ -480,7 +481,7 @@ body
background-color: #ff7c7c background-color: #ff7c7c
.about-container .about-container
background-color: #6a2222 background-color: $medium-red-bg
color: #eee color: #eee
padding: 50px 20px padding: 50px 20px
flex: 1 flex: 1
@ -570,19 +571,23 @@ body
font-weight: bold font-weight: bold
background-color: #282828 background-color: #282828
.updater-page @mixin article-code
font-size: 20px
max-width: 800px
margin: 0 auto
color: #222
code code
font-size: 0.9em font-size: 0.8em
letter-spacing: -0.2px
background: #ccc background: #ccc
color: #000 color: #000
padding: 0px 4px padding: 0px 4px
border-radius: 2px border-radius: 2px
.updater-page
@include article-code
font-size: 20px
max-width: 800px
margin: 0 auto
color: #222
.commits .commits
border-left: 2px solid #555 border-left: 2px solid #555
margin: 10px 0px margin: 10px 0px
@ -595,3 +600,36 @@ body
border-left: 2px solid border-left: 2px solid
padding-left: 8px padding-left: 8px
color: #700 color: #700
.article-page
@include article-code
background-color: #fff4e8
font-size: 22px
line-height: 1.4
color: #222
min-height: 100vh
header
background-color: $medium-red-bg
color: #fff
padding: 40px 10px
line-height: 1.2
h1, h2
text-align: center
margin: 0
h1
font-size: 50px
h2
font-size: 30px
.article-main
max-width: 800px
margin: 0 auto
padding: 20px 20px 100px
a, a:visited
color: $main-theme-link-color

View File

@ -29,6 +29,10 @@ subdirs("pug", async (err, dirs) => {
pinski.muteLogsStartingWith("/videoproxy") pinski.muteLogsStartingWith("/videoproxy")
pinski.muteLogsStartingWith("/static") pinski.muteLogsStartingWith("/static")
for (const route of constants.additional_routes) {
pinski.addRoute(route.web, route.local, route.type)
}
if (constants.tor.enabled) { if (constants.tor.enabled) {
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
} }