First release

This commit is contained in:
Cadence Fish
2020-01-13 01:50:21 +13:00
parent 32e4f3d854
commit 6fd7cc501e
31 changed files with 2759 additions and 348 deletions

17
src/site/api/api.js Normal file
View File

@@ -0,0 +1,17 @@
const {fetchUser} = require("../../lib/collectors")
function reply(statusCode, content) {
return {
statusCode: statusCode,
contentType: "application/json",
content: JSON.stringify(content)
}
}
module.exports = [
{route: "/api/user/(\\w+)", methods: ["GET"], code: async ({fill}) => {
const user = await fetchUser(fill[0])
const data = user.export()
return reply(200, data)
}}
]

61
src/site/api/proxy.js Normal file
View File

@@ -0,0 +1,61 @@
const constants = require("../../lib/constants")
const {request} = require("../../lib/utils/request")
const {proxy} = require("pinski/plugins")
const gm = require("gm")
module.exports = [
{route: "/imageproxy", methods: ["GET"], code: async (input) => {
/** @type {URL} */
// check url param exists
const completeURL = input.url
const params = completeURL.searchParams
if (!params.get("url")) return [400, "Must supply `url` query parameter"]
try {
var url = new URL(params.get("url"))
} catch (e) {
return [400, "`url` query parameter is not a valid URL"]
}
// check url protocol
if (url.protocol !== "https:") return [400, "URL protocol must be `https:`"]
// check url host
if (!["fbcdn.net", "cdninstagram.com"].some(host => url.host.endsWith(host))) return [400, "URL host is not allowed"]
if (!["png", "jpg"].some(ext => url.pathname.endsWith(ext))) return [400, "URL extension is not allowed"]
const width = +params.get("width")
if (typeof width === "number" && !isNaN(width) && width > 0) {
/*
This uses graphicsmagick to force crop the image to a square.
Some thumbnails aren't square and will be stretched on the page without this.
If I cropped the images client side, it would have to be done with CSS background-image, which means no <img srcset>.
*/
return request(url).then(res => {
const image = gm(res.body).gravity("Center").crop(width, width, 0, 0).repage("+")
return {
statusCode: 200,
contentType: "image/jpeg",
headers: {
"Cache-Control": constants.image_cache_control
},
stream: image.stream("jpg")
}
/*
Alternative buffer-based method for sending file:
return new Promise(resolve => {
image.toBuffer((err, buffer) => {
if (err) console.error(err)
resolve({
statusCode: 200,
contentType: "image/jpeg",
content: buffer
})
})
})
*/
})
} else {
return proxy(url, {
"Cache-Control": constants.image_cache_control
})
}
}}
]

14
src/site/api/routes.js Normal file
View File

@@ -0,0 +1,14 @@
const {fetchUser} = require("../../lib/collectors")
const {render} = require("pinski/plugins")
module.exports = [
{route: "/u/(\\w+)", methods: ["GET"], code: async ({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})
}}
]

15
src/site/passthrough.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* @typedef Passthrough
* @property {import("pinski").Pinski} instance
* @property {import("ws").Server} wss
* @property {import("pinski").PugCache} pugCache
*/
void 0
/** @type {Passthrough} */
// @ts-ignore
const passthrough = {
}
module.exports = passthrough

View File

@@ -0,0 +1,5 @@
mixin image(url)
-
let params = new URLSearchParams()
params.set("url", url)
img(src="/imageproxy?"+params.toString())&attributes(attributes)

View File

@@ -0,0 +1,15 @@
//- Needs page, pageIndex
include image.pug
mixin timeline_page(page, pageIndex)
- const pageNumber = pageIndex + 1
if pageNumber > 1
.page-number(id=`page-${pageNumber}`)
span.number Page #{pageNumber}
.timeline-inner
- const suggestedSize = 300
each image in page
- const thumbnail = image.getSuggestedThumbnail(suggestedSize) //- use this as the src in case there are problems with srcset
+image(thumbnail.src)(alt=image.getAlt() width=thumbnail.config_width height=thumbnail.config_height srcset=image.getSrcset() sizes=`${suggestedSize}px`).image

49
src/site/pug/user.pug Normal file
View File

@@ -0,0 +1,49 @@
include includes/timeline_page.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
= `${user.data.full_name} (@${user.data.username}) | Bibliogram`
link(rel="stylesheet" type="text/css" href="/static/css/main.css")
body
.main-divider
header.profile-overview
.profile-sticky
+image(user.data.profile_pic_url)(width="150px" height="150px").pfp
//-
Instagram only uses the above URL, but an HD version is also available:
+image(user.data.profile_pic_url_hd)
h1.full-name= user.data.full_name
h2.username= `@${user.data.username}`
p.bio= user.data.biography
div.profile-counter
span(data-numberformat=user.posts).count #{numberFormat(user.posts)}
|
| posts
div.profile-counter
span(data-numberformat=user.following).count #{numberFormat(user.following)}
|
| following
div.profile-counter
span(data-numberformat=user.followedBy).count #{numberFormat(user.followedBy)}
|
| followed by
main.timeline
each page, pageIndex in user.timeline.pages
+timeline_page(page, pageIndex)
if user.timeline.hasNextPage()
div.next-page-container
-
const nu = new URL(url)
nu.searchParams.set("page", user.timeline.pages.length+1)
a(href=`${nu.search}#page-${user.timeline.pages.length+1}` data-cursor=user.timeline.page_info.end_cursor)#next-page.next-page Next page
else
div.page-number.no-more-pages
span.number No more posts.

137
src/site/sass/main.sass Normal file
View File

@@ -0,0 +1,137 @@
$layout-a-max: 820px
$layout-b-min: 821px
body
margin: 0
padding: 0
font-size: 18px
.main-divider
display: block
@media screen and (min-width: $layout-b-min)
display: grid
grid-template-columns: 235px 1fr
min-height: 100vh
.pfp
border-radius: 50%
@mixin link-button
color: hsl(107, 100%, 21.8%)
background: hsl(87, 78.4%, 80%)
padding: 12px
border-radius: 10px
border: 1px solid hsl(106.9, 49.8%, 46.9%)
line-height: 1
text-decoration: none
&:hover
color: hsl(106.4, 100%, 12.9%)
background: hsl(102.1, 77.2%, 67.3%)
border-color: hsl(104, 51.4%, 43.5%)
.profile-overview
text-align: center
z-index: 1
position: relative
contain: paint // </3 css
line-height: 1
@media screen and (max-width: $layout-a-max)
border-bottom: 1px solid #333
box-shadow: 0px -2px 4px 4px rgba(0, 0, 0, 0.4)
padding-bottom: 25px
@media screen and (min-width: $layout-b-min)
border-right: 1px solid #333
box-shadow: -2px 0px 4px 4px rgba(0, 0, 0, 0.4)
.profile-sticky
position: sticky
top: 0
bottom: 0
padding: 10px
.pfp
margin: 25px 0
.full-name
margin: 0 0 8px
font-size: 30px
.username
margin: 0
font-size: 20px
font-weight: normal
.profile-counter
line-height: 1.3
.count
font-weight: bold
.timeline
--image-size: 260px
$image-size: var(--image-size)
$background: #fff4e8
@media screen and (max-width: $layout-a-max)
--image-size: 120px
background-color: $background
padding: 15px 15px 12vh
.page-number
color: #444
line-height: 1
max-width: 600px
margin: 0px auto
padding: 20px 0px // separate margin and padding for better page hash jump locations
text-align: center
position: relative
.number
position: relative
z-index: 1
padding: 10px
background-color: $background
&::before
position: absolute
display: block
content: ""
left: 0
right: 0
top: 50%
border-top: 1px solid
.next-page-container
margin: 20px 0px
display: flex
justify-content: center
.next-page
@include link-button
font-size: 18px
.timeline-inner
display: flex
justify-content: center
flex-wrap: wrap
margin: 0 auto
.image
$margin: 5px
background-color: rgba(40, 40, 40, 0.25)
margin: $margin
max-width: $image-size
max-height: $image-size
width: 100%
height: 100%
&:hover
$border-width: 3px
margin: $margin - $border-width
border: $border-width solid #111

26
src/site/server.js Normal file
View File

@@ -0,0 +1,26 @@
const {Pinski} = require("pinski")
const {subdirs} = require("node-dir")
const passthrough = require("./passthrough")
const pinski = new Pinski({
port: 10407,
relativeRoot: __dirname
})
subdirs("pug", (err, dirs) => {
if (err) throw err
//pinski.addRoute("/", "pug/index.pug", "pug")
pinski.addRoute("/static/css/main.css", "sass/main.sass", "sass")
pinski.addPugDir("pug", dirs)
pinski.addAPIDir("html/static/js/templates/api")
pinski.addSassDir("sass")
pinski.addAPIDir("api")
pinski.startServer()
pinski.enableWS()
require("pinski/plugins").setInstance(pinski)
Object.assign(passthrough, pinski.getExports())
})