2020-01-26 14:56:59 +00:00
const constants = require ( "../constants" )
const { proxyImage , proxyExtendedOwner } = require ( "../utils/proxyurl" )
const { compile } = require ( "pug" )
const collectors = require ( "../collectors" )
const TimelineBaseMethods = require ( "./TimelineBaseMethods" )
const TimelineChild = require ( "./TimelineChild" )
require ( "../testimports" ) ( collectors , TimelineChild , TimelineBaseMethods )
const rssDescriptionTemplate = compile ( `
p ( style = 'white-space: pre-line' ) = caption
2020-01-26 15:15:53 +00:00
each child in children
img ( alt = child . alt src = child . src width = child . width height = child . height )
2020-01-26 14:56:59 +00:00
` )
class TimelineEntry extends TimelineBaseMethods {
constructor ( ) {
super ( )
/** @type {import("../types").TimelineEntryAll} some properties may not be available yet! */
// @ts-ignore
this . data = { }
2020-01-27 06:03:28 +00:00
const error = new Error ( "TimelineEntry data was not initalised in same event loop (missing __typename)" ) // initialise here for a useful stack trace
2020-01-26 14:56:59 +00:00
setImmediate ( ( ) => { // next event loop
2020-01-27 06:03:28 +00:00
if ( ! this . data . _ _typename ) throw error
2020-01-26 14:56:59 +00:00
} )
/** @type {string} Not available until fetchExtendedOwnerP is called */
this . ownerPfpCacheP = null
/** @type {import("./TimelineChild")[]} Not available until fetchChildren is called */
this . children = null
}
async update ( ) {
2020-01-27 06:03:28 +00:00
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 )
} )
2020-01-26 14:56:59 +00:00
}
/ * *
* General apply function that detects the data format
* /
apply ( data ) {
if ( ! data . display _resources ) {
this . applyN1 ( data )
} else if ( data . thumbnail _resources ) {
this . applyN2 ( data )
} else {
this . applyN3 ( data )
}
}
/ * *
* @ param { import ( "../types" ) . TimelineEntryN1 } data
* /
applyN1 ( data ) {
Object . assign ( this . data , data )
this . fixData ( )
}
/ * *
* @ param { import ( "../types" ) . TimelineEntryN2 } data
* /
applyN2 ( data ) {
Object . assign ( this . data , data )
this . fixData ( )
}
/ * *
* @ param { import ( "../types" ) . TimelineEntryN3 } data
* /
applyN3 ( data ) {
Object . assign ( this . data , data )
this . fixData ( )
}
/ * *
* This should keep the same state when applied multiple times to the same data .
* All mutations should act exactly once and have no effect on already mutated data .
* /
fixData ( ) {
}
getCaption ( ) {
const edge = this . data . edge _media _to _caption . edges [ 0 ]
if ( ! edge ) return null // no caption
else return edge . node . text . replace ( /\u2063/g , "" ) // I don't know why U+2063 INVISIBLE SEPARATOR is in here, but it is, and it causes rendering issues with certain fonts, so let's just remove it.
}
/ * *
* Try to get the first meaningful line or sentence from the caption .
* /
getCaptionIntroduction ( ) {
const caption = this . getCaption ( )
if ( ! caption ) return null
else return caption . split ( "\n" ) [ 0 ] . split ( ". " ) [ 0 ]
}
/ * *
* Alt text is not available for N2 , the caption or a placeholder string will be returned instead .
* @ override
* /
getAlt ( ) {
return this . data . accessibility _caption || this . getCaption ( ) || "No image description available."
}
/ * *
* @ returns { import ( "../types" ) . BasicOwner }
* /
getBasicOwner ( ) {
return this . data . owner
}
/ * *
* Not available on N3 !
* Returns proxied URLs ( P )
* /
getThumbnailSrcsetP ( ) {
if ( this . data . thumbnail _resources ) {
return this . data . thumbnail _resources . map ( tr => {
return ` ${ proxyImage ( tr . src , tr . config _width ) } ${ tr . config _width } w `
} ) . join ( ", " )
} else {
return null
}
}
/ * *
* Not available on N3 !
* Returns proxied URLs ( P )
* @ param { number } size
* @ return { import ( "../types" ) . DisplayResource }
* /
getSuggestedThumbnailP ( size ) {
if ( this . data . thumbnail _resources ) {
let found = null // start with nothing
for ( const tr of this . data . thumbnail _resources ) { // and keep looping up the sizes (sizes come sorted)
found = tr
if ( tr . config _width >= size ) break // don't proceed once we find one large enough
}
return {
config _width : found . config _width ,
config _height : found . config _height ,
src : proxyImage ( found . src , found . config _width ) // force resize to config rather than requested
}
} else {
return null
}
}
getThumbnailSizes ( ) {
2020-01-29 10:08:52 +00:00
return ` (max-width: 820px) 200px, 260px ` // from css :(
2020-01-26 14:56:59 +00:00
}
async fetchChildren ( ) {
// Cached children?
if ( this . children ) return this . children
// Not a gallery? Convert self to a child and return.
if ( this . getType ( ) !== constants . symbols . TYPE _GALLERY ) {
return this . children = [ new TimelineChild ( this . data ) ]
}
// Fetch children if needed
if ( ! this . data . edge _sidecar _to _children ) {
await this . update ( )
}
// Create children
return this . children = this . data . edge _sidecar _to _children . edges . map ( e => new TimelineChild ( e . node ) )
}
/ * *
* Returns a proxied profile pic URL ( P )
* @ returns { Promise < import ( "../types" ) . ExtendedOwner > }
* /
async fetchExtendedOwnerP ( ) {
// Do we just already have the extended owner?
if ( this . data . owner . full _name ) { // this property is on extended owner and not basic owner
const clone = proxyExtendedOwner ( this . data . owner )
this . ownerPfpCacheP = clone . profile _pic _url
return clone
}
// The owner may be in the user cache, so copy from that.
// This could be implemented better.
else if ( collectors . requestCache . hasWithoutClean ( "user/" + this . data . owner . username ) ) {
/** @type {import("./User")} */
const user = collectors . requestCache . getWithoutClean ( "user/" + this . data . owner . username )
this . data . owner = {
id : user . data . id ,
username : user . data . username ,
is _verified : user . data . is _verified ,
full _name : user . data . full _name ,
profile _pic _url : user . data . profile _pic _url // _hd is also available here.
}
const clone = proxyExtendedOwner ( this . data . owner )
this . ownerPfpCacheP = clone . profile _pic _url
return clone
}
// We'll have to re-request ourselves.
else {
await this . update ( )
const clone = proxyExtendedOwner ( this . data . owner )
this . ownerPfpCacheP = clone . profile _pic _url
return clone
}
}
2020-01-26 15:15:53 +00:00
async fetchFeedData ( ) {
const children = await this . fetchChildren ( )
2020-01-26 14:56:59 +00:00
return {
title : this . getCaptionIntroduction ( ) || ` New post from @ ${ this . getBasicOwner ( ) . username } ` ,
2020-01-26 15:15:53 +00:00
description : rssDescriptionTemplate ( {
caption : this . getCaption ( ) ,
children : children . map ( child => ( {
2020-01-29 10:00:47 +00:00
src : ` ${ constants . website _origin } ${ child . getDisplayUrlP ( ) } ` ,
2020-01-26 15:15:53 +00:00
alt : child . getAlt ( ) ,
width : child . data . dimensions . width ,
height : child . data . dimensions . height
} ) )
} ) ,
2020-01-26 14:56:59 +00:00
author : this . data . owner . username ,
2020-01-29 10:00:47 +00:00
url : ` ${ constants . website _origin } /p/ ${ this . data . shortcode } ` ,
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.
2020-01-26 14:56:59 +00:00
date : new Date ( this . data . taken _at _timestamp * 1000 )
/ *
Readers should display the description as HTML rather than using the media enclosure .
enclosure : {
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
}
* /
}
}
}
module . exports = TimelineEntry