2018-07-12 23:07:16 +02:00
"use strict" ; require ( "dotenv" ) . config ( ) ; require ( "date-format-lite" ) ;
2018-06-01 22:23:53 +02:00
2018-03-28 16:26:59 +02:00
2018-07-05 20:50:18 +02:00
// P A C K A G E S
2018-04-10 11:08:46 +02:00
2018-07-30 23:39:32 +02:00
const async = require ( "async" ) ;
2018-08-29 01:57:18 +02:00
const color = require ( "colorette" ) ;
2018-07-05 20:50:18 +02:00
const cors = require ( "cors" ) ;
2018-07-12 23:07:16 +02:00
const dedent = require ( "dedent" ) ;
2018-05-23 16:25:16 +02:00
2018-07-12 17:21:42 +02:00
const fastify = require ( "fastify" ) ( {
logger : {
level : "warn" ,
prettyPrint : process . env . NODE _ENV === "development" ? true : false
}
} ) ;
2018-05-23 16:25:16 +02:00
2018-08-29 01:57:18 +02:00
const html = require ( "choo/html" ) ;
2018-07-18 22:31:05 +02:00
const local = require ( "app-root-path" ) . require ;
2018-07-12 23:07:16 +02:00
const octokit = require ( "@octokit/rest" ) ( ) ;
const redis = require ( "redis" ) ;
2018-07-18 22:31:05 +02:00
const request = require ( "request-promise-native" ) ;
2018-07-12 23:07:16 +02:00
// V A R I A B L E S
2018-08-29 01:57:18 +02:00
const fetchMetadata = local ( "app/helpers/fetch-metadata" ) ;
const github = local ( "app/helpers/github" ) ;
2018-07-12 17:21:42 +02:00
const log = console . log ; // eslint-disable-line
2018-08-29 01:57:18 +02:00
const logSlackError = local ( "app/helpers/slack" ) ;
const relativeDate = local ( "app/modules/relative-date" ) ;
2018-07-12 23:07:16 +02:00
let client ;
if ( typeof process . env . GITHUB _OAUTH _TOKEN !== "undefined" ) {
octokit . authenticate ( {
type : "oauth" ,
token : process . env . GITHUB _OAUTH _TOKEN
} ) ;
2018-07-16 23:06:37 +02:00
} else log ( ` ${ color . red ( "[missing]" ) } GitHub token ` ) ;
2018-07-12 23:07:16 +02:00
if ( typeof process . env . REDISCLOUD _URL !== "undefined" ) {
client = redis . createClient ( process . env . REDISCLOUD _URL ) ;
client . on ( "error" , redisError => {
process . env . NODE _ENV === "development" ?
2018-07-16 23:06:37 +02:00
log ( ` \n ${ color . yellow ( "Unable to connect to Redis client." ) } \n You may be missing an .env file or your connection was reset. ` ) :
2018-07-27 20:04:05 +02:00
logSlackError (
"\n" +
"> *REDIS ERROR:* ```" + JSON . parse ( JSON . stringify ( redisError ) ) + "```" + "\n" +
"> _Cause: Someone is trying to run LBRY.tech locally without environment variables OR Heroku is busted_\n"
)
2018-07-12 23:07:16 +02:00
;
} ) ;
2018-07-16 23:06:37 +02:00
} else log ( ` ${ color . red ( "[missing]" ) } Redis client URL ` ) ;
2018-05-23 16:25:16 +02:00
2018-04-16 15:39:11 +02:00
2018-07-05 20:50:18 +02:00
// P R O G R A M
2018-05-23 16:25:16 +02:00
2018-07-12 17:21:42 +02:00
fastify . use ( cors ( ) ) ;
2018-07-12 23:07:16 +02:00
2018-07-12 17:21:42 +02:00
fastify . register ( require ( "fastify-compress" ) ) ;
fastify . register ( require ( "fastify-ws" ) ) ;
2018-06-08 13:45:56 +02:00
2018-07-12 17:21:42 +02:00
fastify . register ( require ( "fastify-helmet" ) , {
hidePoweredBy : { setTo : "LBRY" }
2018-07-05 20:50:18 +02:00
} ) ;
2018-06-08 13:45:56 +02:00
2018-07-12 17:21:42 +02:00
fastify . register ( require ( "fastify-static" ) , {
2018-08-29 01:57:18 +02:00
prefix : "/assets/" ,
root : ` ${ _ _dirname } /app/dist/ `
2018-07-05 20:50:18 +02:00
} ) ;
2018-05-30 07:50:48 +02:00
2018-07-12 17:21:42 +02:00
fastify . register ( require ( "choo-ssr/fastify" ) , {
2018-08-29 01:57:18 +02:00
app : require ( "./app" )
2018-07-12 17:21:42 +02:00
} ) ;
2018-05-23 16:25:16 +02:00
2018-07-12 17:21:42 +02:00
fastify . ready ( err => {
if ( err ) throw err ;
2018-05-30 17:25:04 +02:00
2018-07-12 17:21:42 +02:00
fastify . ws . on ( "connection" , socket => {
2018-07-16 23:06:37 +02:00
socket . on ( "message" , data => {
data = JSON . parse ( data ) ;
switch ( data . message ) {
2018-07-27 20:04:05 +02:00
case "fetch metadata" :
fetchMetadata ( data , socket ) ;
break ;
2018-07-16 23:06:37 +02:00
case "landed on homepage" :
generateGitHubFeed ( result => {
socket . send ( JSON . stringify ( {
"html" : result ,
2018-07-25 00:29:25 +02:00
"message" : "updated html" ,
2018-07-16 23:06:37 +02:00
"selector" : "#github-feed"
} ) ) ;
} ) ;
break ;
2018-08-11 00:29:06 +02:00
case "landed on tour" :
2018-08-21 23:22:31 +02:00
generateContent ( 1 , result => {
2018-08-08 01:15:34 +02:00
socket . send ( JSON . stringify ( {
"html" : result ,
"message" : "updated html" ,
"selector" : "#tour-loader"
} ) ) ;
} ) ;
break ;
2018-08-11 00:29:06 +02:00
case "request for tour, example 1" :
2018-08-21 23:22:31 +02:00
generateContent ( 1 , result => {
2018-08-11 00:29:06 +02:00
socket . send ( JSON . stringify ( {
"html" : result ,
"message" : "updated html" ,
"selector" : "#tour-loader"
} ) ) ;
} ) ;
break ;
case "request for tour, example 2" :
generateMemeCreator ( socket ) ;
break ;
2018-08-10 00:17:47 +02:00
case "request for tour, example 3" :
2018-08-21 23:22:31 +02:00
generateContent ( 3 , result => {
2018-08-10 00:17:47 +02:00
socket . send ( JSON . stringify ( {
"html" : result ,
"message" : "updated html" ,
"selector" : "#tour-loader"
} ) ) ;
} ) ;
break ;
2018-07-27 20:04:05 +02:00
case "subscribe" :
newsletterSubscribe ( data , socket ) ;
2018-07-16 23:06:37 +02:00
break ;
default :
log ( data ) ;
break ;
2018-07-12 17:21:42 +02:00
}
2018-05-12 12:03:21 +02:00
} ) ;
2018-07-05 20:50:18 +02:00
2018-08-10 00:17:47 +02:00
socket . on ( "close" , ( ) => {
// console.log(socket);
return socket . terminate ( ) ;
} ) ;
2018-07-05 20:50:18 +02:00
} ) ;
2018-07-12 17:21:42 +02:00
} ) ;
2018-06-01 09:10:22 +02:00
2018-07-12 17:21:42 +02:00
// B E G I N
2018-06-01 09:10:22 +02:00
2018-07-12 17:21:42 +02:00
const start = async ( ) => {
try {
2018-09-25 03:16:34 +02:00
await fastify . listen ( process . env . PORT || 8080 , process . env . IP || "0.0.0.0" ) ;
/ *
2018-09-25 01:05:53 +02:00
await fastify . listen (
process . env . NODE _ENV === "development" ?
8080 :
process . env . PORT
) ;
2018-09-25 03:16:34 +02:00
* /
2018-07-12 17:21:42 +02:00
} catch ( err ) {
fastify . log . error ( err ) ;
process . exit ( 1 ) ;
}
2018-06-01 09:10:22 +02:00
2018-07-12 23:07:16 +02:00
process . env . NODE _ENV === "development" ?
2018-07-16 23:06:37 +02:00
log ( ` \n — ${ color . green ( "⚡" ) } ${ fastify . server . address ( ) . port } \n ` ) :
2018-07-12 23:07:16 +02:00
logSlackError ( ` Server started at port \` ${ fastify . server . address ( ) . port } \` ` )
;
2018-07-12 17:21:42 +02:00
} ;
2018-06-06 13:02:16 +02:00
2018-07-12 17:21:42 +02:00
start ( ) ;
2018-07-12 23:07:16 +02:00
// H E L P E R S
2018-07-25 20:35:59 +02:00
function generateGitHubFeed ( displayGitHubFeed ) {
if ( typeof process . env . REDISCLOUD _URL !== "undefined" ) {
client . zrevrange ( "events" , 0 , 9 , ( err , reply ) => {
if ( err ) return ; // TODO: Render a div with nice error message
const events = [ ] ;
const renderedEvents = [ ] ;
reply . forEach ( item => events . push ( JSON . parse ( item ) ) ) ;
for ( const event of events ) {
renderedEvents . push ( `
< div class = 'github-feed__event' >
< a href = "${github.generateUrl(" actor ", event)}" target = "_blank" rel = "noopener noreferrer" >
< img src = "${event.actor.avatar_url}" class = "github-feed__event__avatar" alt = "" / >
< / a >
< p >
$ { github . generateEvent ( event ) }
< a href = "${github.generateUrl(" repo ", event)}" title = "View this repo on GitHub" target = "_blank" rel = "noopener noreferrer" > < strong > $ { event . repo . name } < / s t r o n g > < / a >
< em class = "github-feed__event__time" > $ { relativeDate ( new Date ( event . created _at ) ) } < / e m >
< / p >
< / d i v >
` );
}
2018-07-30 23:39:32 +02:00
updateGithubFeed ( ) ; // TODO: Update `.last-updated` every minute
2018-07-25 20:35:59 +02:00
displayGitHubFeed ( dedent `
< h3 > GitHub < / h 3 >
2018-07-31 00:19:05 +02:00
< h5 class = "last-updated" > Last updated : $ { new Date ( ) . format ( "YYYY-MM-DD" ) . replace ( /-/g , "·" ) } at $ { new Date ( ) . add ( - 4 , "hours" ) . format ( "UTC:H:mm:ss A" ) . toLowerCase ( ) } EST < / h 5 >
2018-07-25 20:35:59 +02:00
$ { renderedEvents . join ( "" ) }
` );
} ) ;
}
}
2018-08-11 00:29:06 +02:00
function generateMemeCreator ( socket ) {
const images = [
{
alt : "Carl Sagan" ,
src : "/assets/media/images/carlsagan2.jpg"
} ,
{
alt : "Doge" ,
src : "/assets/media/images/doge-meme.jpg"
} ,
{
alt : "LBRY Logo With Green Background" ,
src : "/assets/media/images/lbry-green.png"
}
] ;
const memePlaceholderData = {
bottomLine : {
placeholder : "Top line" ,
value : "that I made"
} ,
description : {
placeholder : "Description" ,
value : "Check out this image I published to LBRY via lbry.tech"
} ,
topLine : {
placeholder : "Top line" ,
value : "This is an example meme"
} ,
title : {
placeholder : "Title" ,
value : "Dank Meme Supreme da Cheese"
}
} ;
const renderedImages = [ ] ;
for ( const image of images ) {
renderedImages . push ( ` <img alt=" ${ image . alt } " class="tour__content__meme__canvas__thumbnail" src=" ${ image . src } "/> ` ) ;
}
const memeCreator = html `
< div class = "tour__content__meme__canvas" >
< img alt = "Base image for LBRY meme creator" id = "base-image" style = "height: 0; visibility: hidden;" / >
< canvas id = "meme-canvas" height = "300" width = "400" > Unfortunately , it looks like canvas is < strong > not supported < / s t r o n g > i n y o u r b r o w s e r < / c a n v a s >
$ { renderedImages }
< / d i v >
< form class = "tour__content__meme__editor" >
< h2 > Image Text < / h 2 >
< fieldset >
< label for = "meme-top-line" > Top line < / l a b e l >
< input id = "meme-top-line" name = "meme-top-line" placeholder = "${memePlaceholderData.topLine.placeholder}" spellcheck = "false" type = "text" value = "${memePlaceholderData.topLine.value}" required / >
< / f i e l d s e t >
< fieldset >
< label for = "meme-bottom-line" > Bottom line < / l a b e l >
< input id = "meme-bottom-line" name = "meme-bottom-line" placeholder = "${memePlaceholderData.bottomLine.placeholder}" spellcheck = "false" type = "text" value = "${memePlaceholderData.bottomLine.value}" required / >
< / f i e l d s e t >
< h2 class = "__metadata" > Metadata < / h 2 >
< fieldset >
< label for = "meme-title" > Title < / l a b e l >
< input id = "meme-title" name = "meme-title" placeholder = "${memePlaceholderData.title.placeholder}" spellcheck = "false" type = "text" value = "${memePlaceholderData.title.value}" required / >
< / f i e l d s e t >
< fieldset >
< label for = "meme-description" > Description < / l a b e l >
< textarea id = "meme-description" name = "meme-description" placeholder = "${memePlaceholderData.description.placeholder}" spellcheck = "false" type = "text" required > $ { memePlaceholderData . description . value } < / t e x t a r e a >
< / f i e l d s e t >
< fieldset >
< label for = "meme-language" > Language < / l a b e l >
< select id = "meme-language" name = "meme-language" >
< option value = "ar" > Arabic < / o p t i o n >
< option value = "zh" > Chinese ( Mandarin ) < / o p t i o n >
< option value = "en" > English < / o p t i o n >
< option value = "fr" > French < / o p t i o n >
< option value = "de" > German < / o p t i o n >
< option value = "it" > Italian < / o p t i o n >
< option value = "jp" > Japanese < / o p t i o n >
< option value = "ru" > Russian < / o p t i o n >
< option value = "es" > Spanish < / o p t i o n >
< option value = "" > Not specified < / o p t i o n >
< / s e l e c t >
< / f i e l d s e t >
< fieldset >
< label for = "meme-license" > License < / l a b e l >
< select id = "meme-license" name = "meme-license" required >
< option value = "Public Domain" > Public Domain < / o p t i o n >
< option value = "Creative Commons Attribution 4.0 International" > Creative Commons Attribution 4.0 International < / o p t i o n >
< option value = "Creative Commons Attribution-ShareAlike 4.0 International" > Creative Commons Attribution - ShareAlike 4.0 International < / o p t i o n >
< option value = "Creative Commons Attribution-NoDerivatives 4.0 International" > Creative Commons Attribution - NoDerivatives 4.0 International < / o p t i o n >
< option value = "Creative Commons Attribution-NonCommercial 4.0 International" > Creative Commons Attribution - NonCommercial 4.0 International < / o p t i o n >
< option value = "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International" > Creative Commons Attribution - NonCommercial - ShareAlike 4.0 International < / o p t i o n >
< option value = "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International" > Creative Commons Attribution - NonCommercial - NoDerivatives 4.0 International < / o p t i o n >
< option value = "None" > None < / o p t i o n >
< / s e l e c t >
< / f i e l d s e t >
< fieldset >
< label > < input id = "meme-nsfw-flag" name = "nsfw" type = "checkbox" / > NSFW < / l a b e l >
< / f i e l d s e t >
< fieldset >
< button data - action = "upload image" class = "__button-black" type = "button" > Submit < / b u t t o n >
< / f i e l d s e t >
< / f o r m >
< script >
detectLanguageAndUpdate ( ) ;
initCanvas ( ) ;
setTimeout ( ( ) => {
$ ( ".tour__content__meme__canvas__thumbnail" ) . click ( ) ;
} , 100 ) ;
< / s c r i p t >
` ;
return socket . send ( JSON . stringify ( {
"html" : memeCreator ,
"message" : "updated html" ,
"selector" : "#tour-loader"
} ) ) ;
}
2018-08-21 23:22:31 +02:00
function generateContent ( exampleNumber , displayTrendingContent ) {
if ( exampleNumber === 1 ) {
return getTrendingContent ( ) . then ( response => {
if ( ! response || ! response . success || response . success !== true || ! response . data ) return "" ;
const rawContentCollection = [ ] ;
const renderedContentCollection = [ ] ;
const trendingContentData = response . data ;
for ( const data of trendingContentData ) {
rawContentCollection . push ( fetchMetadata ( { claim : data . url , method : "resolve" , example : exampleNumber } ) ) ;
}
Promise . all ( rawContentCollection ) . then ( collection => {
for ( const part of collection ) {
if (
! part . value . stream . metadata . nsfw &&
part . value . stream . metadata . thumbnail &&
part . channel _name
) {
renderedContentCollection . push ( `
< figure class = "tour__content__trend" >
< img alt = "${part.name}" data - action = "choose claim" data - claim - id = "${exampleNumber === 1 ? part.name : part.claim_id}" src = "${part.value.stream.metadata.thumbnail}" / >
< figcaption data - action = "choose claim" data - claim - id = "${exampleNumber === 1 ? part.name : part.claim_id}" >
$ { part . value . stream . metadata . title }
< span > $ { part . channel _name } < / s p a n >
< / f i g c a p t i o n >
< / f i g u r e >
` );
}
}
displayTrendingContent ( renderedContentCollection . join ( "" ) ) ;
} ) ;
} ) ;
}
if ( exampleNumber === 3 ) {
const approvedUrls = [
"LBRY#3db81c073f82fd1bb670c65f526faea3b8546720" ,
"correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e" ,
"thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6nyte" ,
"epic-arcade-mode-duos-nickatnyte-molt#d81bac6d49b1f92e58c37a5f633a27a45b43405e" ,
"political-correctness-a-force-for-good-a#b4668c0bd096317b44c40738c099b6618095e75f" ,
"10-secrets-hidden-inside-famous-logos#007789cc45cbb4255cf02ba77cbf84ca8e3d7561" ,
"ever-wonder-how-bitcoin-and-other#1ac47b8b3def40a25850dc726a09ce23d09e7009" ,
"bankrupt-pan-am#784b3c215a6f06b663fc1aa292bcb19f29c489bb" ,
"minecraft-in-real-life-iron-man#758dd6497cdfc401ae1f25984738d024d47b50af" ,
"ethan-shows-kyle-warframe-skyvault#8a7401b88d5ed0376d98f16808194d4dcb05b284"
] ;
2018-08-10 00:17:47 +02:00
const rawContentCollection = [ ] ;
const renderedContentCollection = [ ] ;
2018-08-21 23:22:31 +02:00
for ( const url of approvedUrls ) {
rawContentCollection . push ( fetchMetadata ( { claim : url , method : "resolve" , example : exampleNumber } ) ) ;
2018-08-10 00:17:47 +02:00
}
Promise . all ( rawContentCollection ) . then ( collection => {
for ( const part of collection ) {
2018-08-21 23:22:31 +02:00
if (
part &&
part . value &&
part . value . stream . metadata . thumbnail &&
part . channel _name
) {
2018-08-10 00:17:47 +02:00
renderedContentCollection . push ( `
< figure class = "tour__content__trend" >
< img alt = "${part.name}" data - action = "choose claim" data - claim - id = "${exampleNumber === 1 ? part.name : part.claim_id}" src = "${part.value.stream.metadata.thumbnail}" / >
< figcaption data - action = "choose claim" data - claim - id = "${exampleNumber === 1 ? part.name : part.claim_id}" >
$ { part . value . stream . metadata . title }
< span > $ { part . channel _name } < / s p a n >
< / f i g c a p t i o n >
< / f i g u r e >
` );
}
}
displayTrendingContent ( renderedContentCollection . join ( "" ) ) ;
} ) ;
2018-08-21 23:22:31 +02:00
}
2018-08-10 00:17:47 +02:00
}
function getTrendingContent ( ) {
2018-08-11 00:29:06 +02:00
return new Promise ( ( resolve , reject ) => { // eslint-disable-line
2018-08-10 00:17:47 +02:00
request ( {
method : "GET" ,
url : "https://api.lbry.io/file/list_trending"
} , ( error , response , body ) => {
2018-08-11 00:29:06 +02:00
if ( error || ! JSON . parse ( body ) ) resolve ( "Issue fetching content" ) ; // error
2018-08-10 00:17:47 +02:00
body = JSON . parse ( body ) ;
resolve ( body ) ;
} ) ;
} ) ;
}
2018-07-27 20:04:05 +02:00
function newsletterSubscribe ( data , socket ) {
const email = data . email ;
if ( ! validateEmail ( email ) ) return socket . send ( JSON . stringify ( {
"html" : "Your email is invalid" ,
"message" : "updated html" ,
"selector" : "#emailMessage"
} ) ) ;
return new Promise ( ( resolve , reject ) => {
request ( {
method : "POST" ,
2018-09-25 00:11:32 +02:00
url : ` https://api.lbry.io/list/subscribe?email= ${ email } &tag=developer `
2018-07-27 20:04:05 +02:00
} ) . then ( body => {
if ( ! body || ! JSON . parse ( body ) ) {
logSlackError (
"\n" +
"> *NEWSLETTER ERROR:* ```¯\\_(ツ)_/¯ This should be an unreachable error```" + "\n" +
` > _Cause: ${ email } interacted with the form_ \n `
) ;
return resolve ( socket . send ( JSON . stringify ( {
"html" : "Something is terribly wrong" ,
"message" : "updated html" ,
"selector" : "#emailMessage"
} ) ) ) ;
}
body = JSON . parse ( body ) ;
if ( ! body . success ) {
logSlackError (
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON . parse ( JSON . stringify ( body . error ) ) + "```" + "\n" +
` > _Cause: ${ email } interacted with the form_ \n `
) ;
return reject ( socket . send ( JSON . stringify ( {
"html" : body . error ,
"message" : "updated html" ,
"selector" : "#emailMessage"
} ) ) ) ;
}
return resolve ( socket . send ( JSON . stringify ( {
"html" : "Thank you! Please confirm subscription in your inbox." ,
"message" : "updated html" ,
"selector" : "#emailMessage"
} ) ) ) ;
} ) . catch ( welp => {
if ( welp . statusCode === 409 ) {
logSlackError (
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON . parse ( JSON . stringify ( welp . error ) ) + "```" + "\n" +
` > _Cause: ${ email } interacted with the form_ \n `
) ;
return resolve ( socket . send ( JSON . stringify ( {
"html" : "You have already subscribed!" ,
"message" : "updated html" ,
"selector" : "#emailMessage"
} ) ) ) ;
}
} ) ;
} ) ;
}
2018-07-30 23:39:32 +02:00
function updateGithubFeed ( ) {
octokit . activity . getEventsForOrg ( {
org : "lbryio" ,
per _page : 20 ,
page : 1
} ) . then ( ( { data } ) => {
async . eachSeries ( data , ( item , callback ) => {
const eventString = JSON . stringify ( item ) ;
client . zrank ( "events" , eventString , ( err , reply ) => {
if ( reply === null ) client . zadd ( "events" , item . id , eventString , callback ) ;
else callback ( ) ;
} ) ;
} , ( ) => client . zremrangebyrank ( "events" , 0 , - 51 ) ) ; // Keep the latest 50 events
} ) . catch ( err => {
logSlackError (
"\n" +
"> *GITHUB FEED ERROR:* ```" + JSON . parse ( JSON . stringify ( err ) ) + "```" + "\n" +
"> _Cause: GitHub feed refresh_\n"
) ;
} ) ;
}
2018-08-08 01:15:34 +02:00
function validateEmail ( email ) {
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\\.,;:\s@"]{2,})$/i ;
return re . test ( String ( email ) ) ;
}