diff --git a/src/lib/collectors.js b/src/lib/collectors.js index 43ea11a..44dc2fc 100644 --- a/src/lib/collectors.js +++ b/src/lib/collectors.js @@ -10,13 +10,16 @@ const timelineEntryCache = new TtlCache(constants.resource_cache_time) function fetchUser(username) { return requestCache.getOrFetch("user/"+username, () => { - return request(`https://www.instagram.com/${username}/`).then(res => res.text()).then(text => { - // require down here or have to deal with require loop. require cache will take care of it anyway. - // User -> Timeline -> TimelineImage -> collectors -/> User - const User = require("./structures/User") - const sharedData = extractSharedData(text) - const user = new User(sharedData.entry_data.ProfilePage[0].graphql.user) - return user + return request(`https://www.instagram.com/${username}/`).then(res => { + if (res.status === 404) throw constants.symbols.NOT_FOUND + else return res.text().then(text => { + // require down here or have to deal with require loop. require cache will take care of it anyway. + // User -> Timeline -> TimelineImage -> collectors -/> User + const User = require("./structures/User") + const sharedData = extractSharedData(text) + const user = new User(sharedData.entry_data.ProfilePage[0].graphql.user) + return user + }) }) }) } @@ -24,7 +27,7 @@ function fetchUser(username) { /** * @param {string} userID * @param {string} after - * @returns {Promise>} + * @returns {Promise>} */ function fetchTimelinePage(userID, after) { const p = new URLSearchParams() @@ -36,7 +39,7 @@ function fetchTimelinePage(userID, after) { })) return requestCache.getOrFetchPromise("page/"+after, () => { return request(`https://www.instagram.com/graphql/query/?${p.toString()}`).then(res => res.json()).then(root => { - /** @type {import("./types").PagedEdges} */ + /** @type {import("./types").PagedEdges} */ const timeline = root.data.user.edge_owner_to_timeline_media return timeline }) @@ -86,7 +89,12 @@ function fetchShortcodeData(shortcode) { return request(`https://www.instagram.com/graphql/query/?${p.toString()}`).then(res => res.json()).then(root => { /** @type {import("./types").TimelineEntryN3} */ const data = root.data.shortcode_media - return data + if (data == null) { + // the thing doesn't exist + throw constants.symbols.NOT_FOUND + } else { + return data + } }) }) } diff --git a/src/lib/constants.js b/src/lib/constants.js index 5e10500..00a3f53 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -16,6 +16,8 @@ module.exports = { TYPE_VIDEO: Symbol("TYPE_VIDEO"), TYPE_GALLERY: Symbol("TYPE_GALLERY"), TYPE_GALLERY_IMAGE: Symbol("TYPE_GALLERY_IMAGE"), - TYPE_GALLERY_VIDEO: Symbol("TYPE_GALLERY_VIDEO") + TYPE_GALLERY_VIDEO: Symbol("TYPE_GALLERY_VIDEO"), + NOT_FOUND: Symbol("NOT_FOUND"), + NO_SHARED_DATA: Symbol("NO_SHARED_DATA") } } diff --git a/src/lib/structures/TimelineEntry.js b/src/lib/structures/TimelineEntry.js index 0693f1d..97e0eb2 100644 --- a/src/lib/structures/TimelineEntry.js +++ b/src/lib/structures/TimelineEntry.js @@ -19,8 +19,9 @@ class TimelineEntry extends TimelineBaseMethods { /** @type {import("../types").TimelineEntryAll} some properties may not be available yet! */ // @ts-ignore this.data = {} + const error = new Error("TimelineEntry data was not initalised in same event loop (missing __typename)") // initialise here for a useful stack trace setImmediate(() => { // next event loop - if (!this.data.__typename) throw new Error("TimelineEntry data was not initalised in same event loop (missing __typename)") + if (!this.data.__typename) throw error }) /** @type {string} Not available until fetchExtendedOwnerP is called */ this.ownerPfpCacheP = null @@ -29,8 +30,12 @@ class TimelineEntry extends TimelineBaseMethods { } async update() { - const data = await collectors.fetchShortcodeData(this.data.shortcode) - this.applyN3(data) + return collectors.fetchShortcodeData(this.data.shortcode).then(data => { + this.applyN3(data) + }).catch(error => { + console.error("TimelineEntry could not self-update; trying to continue anyway...") + console.error("E:", error) + }) } /** diff --git a/src/lib/utils/body.js b/src/lib/utils/body.js index ad7e1e2..9ae880f 100644 --- a/src/lib/utils/body.js +++ b/src/lib/utils/body.js @@ -1,3 +1,4 @@ +const constants = require("../constants") const {Parser} = require("./parser/parser") /** @@ -5,7 +6,8 @@ const {Parser} = require("./parser/parser") */ function extractSharedData(text) { const parser = new Parser(text) - parser.seek("window._sharedData = ", {moveToMatch: true, useEnd: true}) + const index = parser.seek("window._sharedData = ", {moveToMatch: true, useEnd: true}) + if (index === -1) throw constants.symbols.NO_SHARED_DATA parser.store() const end = parser.seek(";") parser.restore() diff --git a/src/site/api/feed.js b/src/site/api/feed.js index 6c6c6fd..290153d 100644 --- a/src/site/api/feed.js +++ b/src/site/api/feed.js @@ -3,14 +3,25 @@ const {fetchUser} = require("../../lib/collectors") const {render} = require("pinski/plugins") module.exports = [ - {route: `/u/(${constants.external.username_regex})/rss.xml`, methods: ["GET"], code: async ({url, fill}) => { - const user = await fetchUser(fill[0]) - const content = await user.timeline.fetchFeed() - const xml = content.xml() - return { - statusCode: 200, - contentType: "application/rss+xml", // see https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed - content: xml - } + {route: `/u/(${constants.external.username_regex})/rss.xml`, methods: ["GET"], code: ({fill}) => { + return fetchUser(fill[0]).then(async user => { + const content = await user.timeline.fetchFeed() + const xml = content.xml() + return { + statusCode: 200, + contentType: "application/rss+xml", // see https://stackoverflow.com/questions/595616/what-is-the-correct-mime-type-to-use-for-an-rss-feed + content: xml + } + }).catch(error => { + if (error === constants.symbols.NOT_FOUND) { + return render(404, "pug/friendlyerror.pug", { + statusCode: 404, + title: "Not found", + message: "This user doesn't exist." + }) + } else { + throw error + } + }) }} ] diff --git a/src/site/api/routes.js b/src/site/api/routes.js index 43e2efd..9a9bda8 100644 --- a/src/site/api/routes.js +++ b/src/site/api/routes.js @@ -4,39 +4,72 @@ const {render} = require("pinski/plugins") module.exports = [ { - route: `/u/(${constants.external.username_regex})`, methods: ["GET"], code: async ({url, fill}) => { + route: `/u/(${constants.external.username_regex})`, methods: ["GET"], code: ({url, fill}) => { const params = url.searchParams - const user = await fetchUser(fill[0]) - const page = +params.get("page") - if (typeof page === "number" && !isNaN(page) && page >= 1) { - await user.timeline.fetchUpToPage(page - 1) - } - return render(200, "pug/user.pug", {url, user}) + return fetchUser(fill[0]).then(async user => { + const page = +params.get("page") + if (typeof page === "number" && !isNaN(page) && page >= 1) { + await user.timeline.fetchUpToPage(page - 1) + } + return render(200, "pug/user.pug", {url, user}) + }).catch(error => { + if (error === constants.symbols.NOT_FOUND) { + return render(404, "pug/friendlyerror.pug", { + statusCode: 404, + title: "Not found", + message: "This user doesn't exist." + }) + } else { + throw error + } + }) } }, { route: `/fragment/user/(${constants.external.username_regex})/(\\d+)`, methods: ["GET"], code: async ({url, fill}) => { - const user = await fetchUser(fill[0]) - const pageNumber = +fill[1] - const pageIndex = pageNumber - 1 - await user.timeline.fetchUpToPage(pageIndex) - if (user.timeline.pages[pageIndex]) { - return render(200, "pug/fragments/timeline_page.pug", {page: user.timeline.pages[pageIndex], pageIndex, user, url}) - } else { - return { - statusCode: 400, - contentType: "text/html", - content: "That page does not exist" + return fetchUser(fill[0]).then(async user => { + const pageNumber = +fill[1] + const pageIndex = pageNumber - 1 + await user.timeline.fetchUpToPage(pageIndex) + if (user.timeline.pages[pageIndex]) { + return render(200, "pug/fragments/timeline_page.pug", {page: user.timeline.pages[pageIndex], pageIndex, user, url}) + } else { + return { + statusCode: 400, + contentType: "text/html", + content: "That page does not exist" + } } - } + }).catch(error => { + if (error === constants.symbols.NOT_FOUND) { + return render(404, "pug/friendlyerror.pug", { + statusCode: 404, + title: "Not found", + message: "This user doesn't exist." + }) + } else { + throw error + } + }) } }, { - route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: async ({fill}) => { - const post = await getOrFetchShortcode(fill[0]) - await post.fetchChildren() - await post.fetchExtendedOwnerP() // parallel await is okay since intermediate fetch result is cached - return render(200, "pug/post.pug", {post}) + route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => { + return getOrFetchShortcode(fill[0]).then(async post => { + await post.fetchChildren() + await post.fetchExtendedOwnerP() // parallel await is okay since intermediate fetch result is cached + return render(200, "pug/post.pug", {post}) + }).catch(error => { + if (error === constants.symbols.NOT_FOUND) { + return render(404, "pug/friendlyerror.pug", { + statusCode: 404, + title: "Not found", + message: "Somehow, you reached a post that doesn't exist." + }) + } else { + throw error + } + }) } } ] diff --git a/src/site/html/static/js/pagination.js b/src/site/html/static/js/pagination.js index 63a7673..f0cef05 100644 --- a/src/site/html/static/js/pagination.js +++ b/src/site/html/static/js/pagination.js @@ -36,7 +36,6 @@ class NextPage extends FreezeWidth { fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => { q("#next-page-container").remove() this.observer.disconnect() - q("#timeline").insertAdjacentHTML("beforeend", text) addNextPageControl() }) diff --git a/src/site/pug/friendlyerror.pug b/src/site/pug/friendlyerror.pug new file mode 100644 index 0000000..b8138a2 --- /dev/null +++ b/src/site/pug/friendlyerror.pug @@ -0,0 +1,18 @@ +//- Needs title, message, statusCode + +include includes/timeline_page.pug +include includes/next_page_button.pug + +- const numberFormat = new Intl.NumberFormat().format + +doctype html +html + head + meta(charset="utf-8") + meta(name="viewport" content="width=device-width, initial-scale=1") + title= `${title} | Bibliogram` + link(rel="stylesheet" type="text/css" href="/static/css/main.css") + body.error-page + h1.code= statusCode + p.message= message + a(href="javascript:history.back()").back ← Go back? diff --git a/src/site/sass/main.sass b/src/site/sass/main.sass index 13db83d..a2c0127 100644 --- a/src/site/sass/main.sass +++ b/src/site/sass/main.sass @@ -242,3 +242,32 @@ body &:not(:last-child) margin-bottom: 10px + +.error-page + min-height: 100vh + background: #191919 + padding: 10px + text-align: center + display: flex + flex-direction: column + justify-content: center + align-items: center + + .code, .message, .back-link + line-height: 1.2 + margin: 0px + color: white + + .code + font-size: 80px + color: #fff + margin-bottom: 25px + + .message + font-size: 35px + color: #ccc + margin-bottom: 15vh + + .back + color: #4a93d2 + font-size: 25px