1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-22 08:07:30 +00:00

Provide error page for age gated profiles

This commit is contained in:
Cadence Ember 2020-04-14 03:46:23 +12:00
parent 42cede08e5
commit fff2d74fe3
No known key found for this signature in database
GPG Key ID: 128B99B1B74A6412
10 changed files with 418 additions and 36 deletions

View File

@ -101,32 +101,41 @@ function fetchUserFromHTML(username) {
// require down here or have to deal with require loop. require cache will take care of it anyway. // require down here or have to deal with require loop. require cache will take care of it anyway.
// User -> Timeline -> TimelineEntry -> collectors -/> User // User -> Timeline -> TimelineEntry -> collectors -/> User
const User = require("./structures/User") const User = require("./structures/User")
const sharedData = extractSharedData(text) const result = extractSharedData(text)
const user = new User(sharedData.entry_data.ProfilePage[0].graphql.user) if (result.status === constants.symbols.extractor_results.SUCCESS) {
history.report("user", true) const sharedData = result.value
if (constants.caching.db_user_id) { const user = new User(sharedData.entry_data.ProfilePage[0].graphql.user)
const existing = db.prepare("SELECT created, updated_version FROM Users WHERE username = ?").get(user.data.username) history.report("user", true)
db.prepare( if (constants.caching.db_user_id) {
"REPLACE INTO Users (username, user_id, created, updated, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url) VALUES " const existing = db.prepare("SELECT created, updated_version FROM Users WHERE username = ?").get(user.data.username)
+"(@username, @user_id, @created, @updated, @updated_version, @biography, @post_count, @following_count, @followed_by_count, @external_url, @full_name, @is_private, @is_verified, @profile_pic_url)" db.prepare(
).run({ "REPLACE INTO Users (username, user_id, created, updated, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url) VALUES "
username: user.data.username, +"(@username, @user_id, @created, @updated, @updated_version, @biography, @post_count, @following_count, @followed_by_count, @external_url, @full_name, @is_private, @is_verified, @profile_pic_url)"
user_id: user.data.id, ).run({
created: existing && existing.updated_version === constants.database_version ? existing.created : Date.now(), username: user.data.username,
updated: Date.now(), user_id: user.data.id,
updated_version: constants.database_version, created: existing && existing.updated_version === constants.database_version ? existing.created : Date.now(),
biography: user.data.biography || null, updated: Date.now(),
post_count: user.posts || 0, updated_version: constants.database_version,
following_count: user.following || 0, biography: user.data.biography || null,
followed_by_count: user.followedBy || 0, post_count: user.posts || 0,
external_url: user.data.external_url || null, following_count: user.following || 0,
full_name: user.data.full_name || null, followed_by_count: user.followedBy || 0,
is_private: +user.data.is_private, external_url: user.data.external_url || null,
is_verified: +user.data.is_verified, full_name: user.data.full_name || null,
profile_pic_url: user.data.profile_pic_url is_private: +user.data.is_private,
}) is_verified: +user.data.is_verified,
profile_pic_url: user.data.profile_pic_url
})
}
return user
} else if (result.status === constants.symbols.extractor_results.AGE_RESTRICTED) {
// I don't like this code.
history.report("user", true)
throw constants.symbols.extractor_results.AGE_RESTRICTED
} else {
throw result.status
} }
return user
} }
}).catch(error => { }).catch(error => {
if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN || error === constants.symbols.RATE_LIMITED) { if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN || error === constants.symbols.RATE_LIMITED) {

View File

@ -100,11 +100,15 @@ let constants = {
TYPE_GALLERY_IMAGE: Symbol("TYPE_GALLERY_IMAGE"), 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"), NOT_FOUND: Symbol("NOT_FOUND"),
NO_SHARED_DATA: Symbol("NO_SHARED_DATA"),
INSTAGRAM_DEMANDS_LOGIN: Symbol("INSTAGRAM_DEMANDS_LOGIN"), INSTAGRAM_DEMANDS_LOGIN: Symbol("INSTAGRAM_DEMANDS_LOGIN"),
RATE_LIMITED: Symbol("RATE_LIMITED"), RATE_LIMITED: Symbol("RATE_LIMITED"),
ENDPOINT_OVERRIDDEN: Symbol("ENDPOINT_OVERRIDDEN"), ENDPOINT_OVERRIDDEN: Symbol("ENDPOINT_OVERRIDDEN"),
NO_ASSISTANTS_AVAILABLE: Symbol("NO_ASSISTANTS_AVAILABLE"), NO_ASSISTANTS_AVAILABLE: Symbol("NO_ASSISTANTS_AVAILABLE"),
extractor_results: {
SUCCESS: Symbol("SUCCESS"),
AGE_RESTRICTED: Symbol("AGE_RESTRICTED"),
NO_SHARED_DATA: Symbol("NO_SHARED_DATA")
},
assistant_statuses: { assistant_statuses: {
OFFLINE: Symbol("OFFLINE"), OFFLINE: Symbol("OFFLINE"),
BLOCKED: Symbol("BLOCKED"), BLOCKED: Symbol("BLOCKED"),

View File

@ -3,17 +3,40 @@ const {Parser} = require("./parser/parser")
/** /**
* @param {string} text * @param {string} text
* @returns {{status: symbol, value: any}}
*/ */
function extractSharedData(text) { function extractSharedData(text) {
const parser = new Parser(text) const parser = new Parser(text)
const index = 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 if (index === -1) {
// Maybe the profile is age restricted?
const age = getRestrictedAge(text)
if (age !== null) { // Correct.
return {status: constants.symbols.extractor_results.AGE_RESTRICTED, value: age}
}
return {status: constants.symbols.extractor_results.NO_SHARED_DATA, value: null}
}
parser.store() parser.store()
const end = parser.seek(";</script>") const end = parser.seek(";</script>")
parser.restore() parser.restore()
const sharedDataString = parser.slice(end - parser.cursor) const sharedDataString = parser.slice(end - parser.cursor)
const sharedData = JSON.parse(sharedDataString) const sharedData = JSON.parse(sharedDataString)
return sharedData return {status: constants.symbols.extractor_results.SUCCESS, value: sharedData}
}
/**
* @param {string} text
*/
function getRestrictedAge(text) {
const parser = new Parser(text)
let index = parser.seek("<h2>Restricted profile</h2>", {moveToMatch: true, useEnd: true})
if (index === -1) return null
index = parser.seek("<p>", {moveToMatch: true, useEnd: true})
if (index === -1) return null
const explanation = parser.get({split: "</p>"}).trim()
const match = explanation.match(/You must be (\d+?) years? old or over to see this profile/)
if (!match) return null
return +match[1] // the age
} }
module.exports.extractSharedData = extractSharedData module.exports.extractSharedData = extractSharedData

View File

@ -37,6 +37,7 @@ class Parser {
} }
/** /**
* Get the next element from the buffer, either up to a token or between two tokens, and update the cursor.
* @param {GetOptions} [options] * @param {GetOptions} [options]
* @returns {String} * @returns {String}
*/ */
@ -123,7 +124,7 @@ class Parser {
} }
/** /**
* Seek past the next occurance of the string. * Seek to or past the next occurance of the string.
* @param {string} toFind * @param {string} toFind
* @param {{moveToMatch?: boolean, useEnd?: boolean}} options both default to false * @param {{moveToMatch?: boolean, useEnd?: boolean}} options both default to false
*/ */

View File

@ -47,6 +47,8 @@ module.exports = [
expiresMinutes: userRequestCache.getTtl("user/"+fill[0], 1000*60) 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 { } else {
throw error throw error
} }

View File

@ -89,6 +89,8 @@ module.exports = [
expiresMinutes: userRequestCache.getTtl("user/"+fill[0], 1000*60) 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 { } else {
throw error throw error
} }

View File

@ -0,0 +1,11 @@
include includes/error.pug
doctype html
html
head
title= `Restricted profile | Bibliogram`
include includes/head
body.error-page
+error(403, "Restricted profile", false)
| This profile is age restricted.
| You must log in to Instagram to view this profile.

View File

@ -2,8 +2,6 @@
include includes/error.pug include includes/error.pug
- const numberFormat = new Intl.NumberFormat().format
doctype html doctype html
html html
head head

View File

@ -4,16 +4,27 @@ const {extractSharedData} = require("../src/lib/utils/body")
const fs = require("fs").promises const fs = require("fs").promises
tap.test("extract shared data", async childTest => { tap.test("extract shared data", async childTest => {
childTest.throws(() => extractSharedData(""), constants.symbols.NO_SHARED_DATA, "not found in blank") {
const result = extractSharedData("")
childTest.equal(result.status, constants.symbols.extractor_results.NO_SHARED_DATA, "not found in blank")
}
{ {
const page = await fs.readFile("test/files/page-user-instagram.html", "utf8") const page = await fs.readFile("test/files/page-user-instagram.html", "utf8")
const sharedData = extractSharedData(page) const result = extractSharedData(page)
childTest.equal(sharedData.entry_data.ProfilePage[0].graphql.user.username, "instagram", "can extract user page") childTest.equal(result.status, constants.symbols.extractor_results.SUCCESS, "extractor status success")
childTest.equal(result.value.entry_data.ProfilePage[0].graphql.user.username, "instagram", "can extract user page")
} }
{ {
const page = await fs.readFile("test/files/page-login.html", "utf8") const page = await fs.readFile("test/files/page-login.html", "utf8")
const sharedData = extractSharedData(page) const result = extractSharedData(page)
childTest.true(sharedData.entry_data.LoginAndSignupPage[0], "can extract login page") childTest.equal(result.status, constants.symbols.extractor_results.SUCCESS, "extractor status success")
childTest.true(result.value.entry_data.LoginAndSignupPage[0], "can extract login page")
}
{
const page = await fs.readFile("test/files/page-age-gated.html", "utf8")
const result = extractSharedData(page)
childTest.equal(result.status, constants.symbols.extractor_results.AGE_RESTRICTED, "extractor detects age restricted")
childTest.equal(result.value, 21, "correct age is extracted")
} }
childTest.end() childTest.end()
}) })

File diff suppressed because one or more lines are too long