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" )
2020-05-30 11:04:06 +00:00
const { structure , removeTrailingHashtags } = require ( "../utils/structuretext" )
2020-01-26 14:56:59 +00:00
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
2020-05-30 06:59:59 +00:00
this . date = null
2020-01-26 14:56:59 +00:00
}
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 ( ) {
2020-05-30 06:59:59 +00:00
this . date = new Date ( this . data . taken _at _timestamp * 1000 )
}
getDisplayDate ( ) {
function pad ( number ) {
return String ( number ) . padStart ( 2 , "0" )
}
return (
` ${ this . date . getUTCFullYear ( ) } `
+ ` - ${ pad ( this . date . getUTCMonth ( ) + 1 ) } `
2020-05-31 06:06:08 +00:00
+ ` - ${ pad ( this . date . getUTCDate ( ) ) } `
2020-05-30 06:59:59 +00:00
+ ` ${ pad ( this . date . getUTCHours ( ) ) } `
+ ` : ${ pad ( this . date . getUTCMinutes ( ) ) } `
+ ` UTC `
)
2020-01-26 14:56:59 +00:00
}
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.
}
2020-02-03 14:30:19 +00:00
getStructuredCaption ( ) {
const caption = this . getCaption ( )
if ( ! caption ) return null // no caption
else return structure ( caption )
}
2020-05-30 11:04:06 +00:00
getStructuredCaptionWithoutTrailingHashtags ( ) {
const structured = this . getStructuredCaption ( )
if ( ! structured ) return null // no caption
else return removeTrailingHashtags ( structured )
}
2020-01-26 14:56:59 +00:00
/ * *
* 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 ) ]
}
2020-04-16 13:15:21 +00:00
/** @type {import("../types").Edges<import("../types").GraphChildN1>|import("../types").Edges<import("../types").GraphChildVideoN3>} */
// @ts-ignore
const children = this . data . edge _sidecar _to _children
// It's a gallery, so we may need to fetch its children
// We need to fetch children if one of them is a video, because N1 has no video_url.
if ( ! children || ! children . edges . length || children . edges . some ( edge => edge . node . is _video && ! edge . node . video _url ) ) {
2020-01-26 14:56:59 +00:00
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.
2020-02-02 14:53:37 +00:00
else if ( collectors . userRequestCache . hasNotPromise ( "user/" + this . data . owner . username ) ) {
2020-01-26 14:56:59 +00:00
/** @type {import("./User")} */
2020-02-02 14:53:37 +00:00
const user = collectors . userRequestCache . getWithoutClean ( "user/" + this . data . owner . username )
2020-02-02 13:24:14 +00:00
if ( user . data . full _name ) {
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
2020-01-26 14:56:59 +00:00
}
2020-02-02 13:24:14 +00:00
// That didn't work, so just fall through...
2020-01-26 14:56:59 +00:00
}
// We'll have to re-request ourselves.
2020-02-02 13:24:14 +00:00
await this . update ( )
const clone = proxyExtendedOwner ( this . data . owner )
this . ownerPfpCacheP = clone . profile _pic _url
return clone
2020-01-26 14:56:59 +00:00
}
2020-01-29 15:20:20 +00:00
fetchVideoURL ( ) {
if ( ! this . isVideo ( ) ) return Promise . resolve ( null )
else if ( this . data . video _url ) return Promise . resolve ( this . getVideoUrlP ( ) )
else return this . update ( ) . then ( ( ) => this . getVideoUrlP ( ) )
}
2020-02-18 00:39:20 +00:00
/ * *
* @ returns { Promise < import ( "feed/src/typings/index" ) . Item > }
* /
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-02-18 00:39:20 +00:00
link : ` ${ constants . website _origin } /p/ ${ this . data . shortcode } ` ,
id : ` bibliogram:post/ ${ this . data . shortcode } ` , // Is it wise to keep the origin in here? The same post would have a different ID from different servers.
published : new Date ( this . data . taken _at _timestamp * 1000 ) , // first published date
date : new Date ( this . data . taken _at _timestamp * 1000 ) // last modified date
2020-01-26 14:56:59 +00:00
/ *
Readers should display the description as HTML rather than using the media enclosure .
enclosure : {
url : this . data . display _url ,
2020-04-22 11:59:45 +00:00
type : "image/jpeg" // Instagram only has JPEGs as far as I can tell
2020-01-26 14:56:59 +00:00
}
* /
}
}
}
module . exports = TimelineEntry