diff --git a/README.md b/README.md index f27c9835..5669b79c 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,23 @@ spee.ch is a single-serving site that reads and publishes images and videos to a ## API #### GET -* /api/resolve/:name - * example: `curl https://spee.ch/api/resolve/doitlive` -* /api/claim_list/:name - * example: `curl https://spee.ch/api/claim_list/doitlive` -* /api/isClaimAvailable/:name (returns `true`/`false` for whether a name is available through spee.ch) - * example: `curl https://spee.ch/api/isClaimAvailable/doitlive` +* /api/claim-resolve/:name + * example: `curl https://spee.ch/api/claim-resolve/doitlive` +* /api/claim-list/:name + * example: `curl https://spee.ch/api/claim-list/doitlive` +* /api/claim-is-available/:name ( + * returns `true`/`false` for whether a name is available through spee.ch + * example: `curl https://spee.ch/api/claim-is-available/doitlive` +* /api/channel-is-available/:name ( + * returns `true`/`false` for whether a channel is available through spee.ch + * example: `curl https://spee.ch/api/channel-is-available/@CoolChannel` #### POST -* /api/publish - * example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/publish` +* /api/claim-publish + * example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim-publish` * Parameters: * `name` - * `file` (.mp4, .jpeg, .jpg, .gif, or .png) + * `file` (must be type .mp4, .jpeg, .jpg, .gif, or .png) * `nsfw` (optional) * `license` (optional) * `title` (optional) diff --git a/config/slackConfig.js b/config/slackConfig.js index 84ab3376..109671f2 100644 --- a/config/slackConfig.js +++ b/config/slackConfig.js @@ -4,27 +4,30 @@ const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook; module.exports = (winston) => { if (config.logging.slackWebHook) { // add a transport for errors to slack - winston.add(winstonSlackWebHook, { - name : 'slack-errors-transport', - level : 'warn', - webhookUrl: config.logging.slackWebHook, - channel : config.logging.slackErrorChannel, - username : 'spee.ch', - iconEmoji : ':face_with_head_bandage:', - }); - winston.add(winstonSlackWebHook, { - name : 'slack-info-transport', - level : 'info', - webhookUrl: config.logging.slackWebHook, - channel : config.logging.slackInfoChannel, - username : 'spee.ch', - iconEmoji : ':nerd_face:', - }); + if (config.logging.slackErrorChannel) { + winston.add(winstonSlackWebHook, { + name : 'slack-errors-transport', + level : 'warn', + webhookUrl: config.logging.slackWebHook, + channel : config.logging.slackErrorChannel, + username : 'spee.ch', + iconEmoji : ':face_with_head_bandage:', + }); + }; + if (config.logging.slackInfoChannel) { + winston.add(winstonSlackWebHook, { + name : 'slack-info-transport', + level : 'info', + webhookUrl: config.logging.slackWebHook, + channel : config.logging.slackInfoChannel, + username : 'spee.ch', + iconEmoji : ':nerd_face:', + }); + }; // send test message winston.error('Slack "error" logging is online.'); - winston.warn('Slack "warning" logging is online.'); winston.info('Slack "info" logging is online.'); } else { - winston.warn('Slack logging is not enabled because no SLACK_WEB_HOOK env var provided.'); + winston.warn('Slack logging is not enabled because no slackWebHook config var provided.'); } }; diff --git a/controllers/serveController.js b/controllers/serveController.js index 233906c5..e2e9ff13 100644 --- a/controllers/serveController.js +++ b/controllers/serveController.js @@ -1,253 +1,90 @@ -const lbryApi = require('../helpers/lbryApi.js'); const db = require('../models'); const logger = require('winston'); -const { serveFile, showFile, showFileLite } = require('../helpers/serveHelpers.js'); -const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js'); +const { returnPaginatedChannelViewData } = require('../helpers/channelPagination.js'); -const SERVE = 'SERVE'; -const SHOW = 'SHOW'; -const SHOWLITE = 'SHOWLITE'; -const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png'; const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; - -function checkForLocalAssetByClaimId (claimId, name) { - logger.debug(`checkForLocalAssetsByClaimId(${claimId}, ${name}`); - return new Promise((resolve, reject) => { - db.File - .findOne({where: { name, claimId }}) - .then(result => { - if (result) { - resolve(result.dataValues); - } else { - resolve(null); - } - }) - .catch(error => { - reject(error); - }); - }); -} - -function addGetResultsToFileRecord (fileInfo, getResult) { - fileInfo.fileName = getResult.file_name; - fileInfo.filePath = getResult.download_path; - fileInfo.fileType = getResult.mime_type; - return fileInfo; -} - -function createFileRecord ({ name, claimId, outpoint, height, address, nsfw }) { - return { - name, - claimId, - outpoint, - height, - address, - fileName: '', - filePath: '', - fileType: '', - nsfw, - }; -} - -function getAssetByLongClaimId (fullClaimId, name) { - logger.debug('...getting asset by claim Id...'); - return new Promise((resolve, reject) => { - // 1. check locally for claim - checkForLocalAssetByClaimId(fullClaimId, name) - .then(dataValues => { - // if a result was found, return early with the result - if (dataValues) { - logger.debug('found a local file for this name and claimId'); - resolve(dataValues); - return; - } - logger.debug('no local file found for this name and claimId'); - // 2. if no local claim, resolve and get the claim - db.Claim - .resolveClaim(name, fullClaimId) - .then(resolveResult => { - // if no result, return early (claim doesn't exist or isn't free) - if (!resolveResult) { - resolve(NO_CLAIM); - return; - } - logger.debug('resolve result >> ', resolveResult.dataValues); - let fileRecord = {}; - // get the claim - lbryApi.getClaim(`${name}#${fullClaimId}`) - .then(getResult => { - logger.debug('getResult >>', getResult); - fileRecord = createFileRecord(resolveResult); - fileRecord = addGetResultsToFileRecord(fileRecord, getResult); - // insert a record in the File table & Update Claim table - return db.File.create(fileRecord); - }) - .then(() => { - logger.debug('File record successfully updated'); - resolve(fileRecord); - }) - .catch(error => { - reject(error); - }); - }) - .catch(error => { - reject(error); - }); - }) - .catch(error => { - reject(error); - }); - }); -} - -function chooseThumbnail (claimInfo, defaultThumbnail) { - if (!claimInfo.thumbnail || claimInfo.thumbnail.trim() === '') { - return defaultThumbnail; - } - return claimInfo.thumbnail; -} +const NO_FILE = 'NO_FILE'; module.exports = { - getAssetByClaim (claimName, claimId) { - logger.debug(`getAssetByClaim(${claimName}, ${claimId})`); - return new Promise((resolve, reject) => { - db.Claim.getLongClaimId(claimName, claimId) // 1. get the long claim id - .then(result => { // 2. get the asset using the long claim id - logger.debug('long claim id ===', result); - if (result === NO_CLAIM) { - logger.debug('resolving NO_CLAIM'); - resolve(NO_CLAIM); - return; - } - resolve(getAssetByLongClaimId(result, claimName)); - }) - .catch(error => { - reject(error); - }); - }); - }, - getAssetByChannel (channelName, channelId, claimName) { - logger.debug('getting asset by channel'); - return new Promise((resolve, reject) => { - db.Certificate.getLongChannelId(channelName, channelId) // 1. get the long channel id - .then(result => { // 2. get the long claim Id - if (result === NO_CHANNEL) { - resolve(NO_CHANNEL); - return; - } - return db.Claim.getClaimIdByLongChannelId(result, claimName); - }) - .then(result => { // 3. get the asset using the long claim id - logger.debug('asset claim id =', result); - if (result === NO_CHANNEL || result === NO_CLAIM) { - resolve(result); - return; - } - resolve(getAssetByLongClaimId(result, claimName)); - }) - .catch(error => { - reject(error); - }); - }); - }, - getChannelContents (channelName, channelId) { - return new Promise((resolve, reject) => { - let longChannelId; - let shortChannelId; - db.Certificate.getLongChannelId(channelName, channelId) // 1. get the long channel Id - .then(result => { // 2. get all claims for that channel - if (result === NO_CHANNEL) { - return NO_CHANNEL; - } - longChannelId = result; - return db.Certificate.getShortChannelIdFromLongChannelId(longChannelId, channelName); - }) - .then(result => { // 3. get all Claim records for this channel - if (result === NO_CHANNEL) { - return NO_CHANNEL; - } - shortChannelId = result; - return db.Claim.getAllChannelClaims(longChannelId); - }) - .then(result => { // 4. add extra data not available from Claim table - if (result === NO_CHANNEL) { - resolve(NO_CHANNEL); - return; - } - if (result) { - result.forEach(element => { - const fileExtenstion = element.contentType.substring(element.contentType.lastIndexOf('/') + 1); - element['showUrlLong'] = `/${channelName}:${longChannelId}/${element.name}`; - element['directUrlLong'] = `/${channelName}:${longChannelId}/${element.name}.${fileExtenstion}`; - element['showUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}`; - element['directUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}.${fileExtenstion}`; - element['thumbnail'] = chooseThumbnail(element, DEFAULT_THUMBNAIL); - }); - } - resolve({ - channelName, - longChannelId, - shortChannelId, - claims: result, - }); - }) - .catch(error => { - reject(error); - }); - }); - }, - serveOrShowAsset (fileInfo, extension, method, headers, originalUrl, ip, res) { - // add file extension to the file info - if (extension === 'gifv') { - fileInfo['fileExt'] = 'gifv'; + getClaimId (channelName, channelClaimId, name, claimId) { + if (channelName) { + return module.exports.getClaimIdByChannel(channelName, channelClaimId, name); } else { - fileInfo['fileExt'] = fileInfo.fileName.substring(fileInfo.fileName.lastIndexOf('.') + 1); - } - // add a record to the stats table - postToStats(method, originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success'); - // serve or show - switch (method) { - case SERVE: - serveFile(fileInfo, res); - sendGoogleAnalytics(method, headers, ip, originalUrl); - return fileInfo; - case SHOWLITE: - return db.Claim.resolveClaim(fileInfo.name, fileInfo.claimId) - .then(claimRecord => { - fileInfo['title'] = claimRecord.title; - fileInfo['description'] = claimRecord.description; - showFileLite(fileInfo, res); - return fileInfo; - }) - .catch(error => { - logger.error('throwing serverOrShowAsset SHOWLITE error...'); - throw error; - }); - case SHOW: - return db.Claim - .getShortClaimIdFromLongClaimId(fileInfo.claimId, fileInfo.name) - .then(shortId => { - fileInfo['shortId'] = shortId; - return db.Claim.resolveClaim(fileInfo.name, fileInfo.claimId); - }) - .then(resolveResult => { - logger.debug('resolve result >>', resolveResult.dataValues); - fileInfo['thumbnail'] = chooseThumbnail(resolveResult, DEFAULT_THUMBNAIL); - fileInfo['title'] = resolveResult.title; - fileInfo['description'] = resolveResult.description; - if (resolveResult.certificateId) { fileInfo['certificateId'] = resolveResult.certificateId }; - if (resolveResult.channelName) { fileInfo['channelName'] = resolveResult.channelName }; - showFile(fileInfo, res); - return fileInfo; - }) - .catch(error => { - logger.error('throwing serverOrShowAsset SHOW error...'); - throw error; - }); - default: - logger.error('I did not recognize that method'); - break; + return module.exports.getClaimIdByClaim(name, claimId); } }, + getClaimIdByClaim (claimName, claimId) { + logger.debug(`getClaimIdByClaim(${claimName}, ${claimId})`); + return new Promise((resolve, reject) => { + db.Claim.getLongClaimId(claimName, claimId) + .then(longClaimId => { + if (!longClaimId) { + resolve(NO_CLAIM); + } + resolve(longClaimId); + }) + .catch(error => { + reject(error); + }); + }); + }, + getClaimIdByChannel (channelName, channelClaimId, claimName) { + logger.debug(`getClaimIdByChannel(${channelName}, ${channelClaimId}, ${claimName})`); + return new Promise((resolve, reject) => { + db.Certificate.getLongChannelId(channelName, channelClaimId) // 1. get the long channel id + .then(longChannelId => { + if (!longChannelId) { + return [null, null]; + } + return Promise.all([longChannelId, db.Claim.getClaimIdByLongChannelId(longChannelId, claimName)]); // 2. get the long claim id + }) + .then(([longChannelId, longClaimId]) => { + if (!longChannelId) { + return resolve(NO_CHANNEL); + } + if (!longClaimId) { + return resolve(NO_CLAIM); + } + resolve(longClaimId); + }) + .catch(error => { + reject(error); + }); + }); + }, + getChannelViewData (channelName, channelClaimId, query) { + return new Promise((resolve, reject) => { + // 1. get the long channel Id (make sure channel exists) + db.Certificate.getLongChannelId(channelName, channelClaimId) + .then(longChannelClaimId => { + if (!longChannelClaimId) { + return [null, null, null]; + } + // 2. get the short ID and all claims for that channel + return Promise.all([longChannelClaimId, db.Certificate.getShortChannelIdFromLongChannelId(longChannelClaimId, channelName), db.Claim.getAllChannelClaims(longChannelClaimId)]); + }) + .then(([longChannelClaimId, shortChannelClaimId, channelClaimsArray]) => { + if (!longChannelClaimId) { + return resolve(NO_CHANNEL); + } + // 3. format the data for the view, including pagination + let paginatedChannelViewData = returnPaginatedChannelViewData(channelName, longChannelClaimId, shortChannelClaimId, channelClaimsArray, query); + // 4. return all the channel information and contents + resolve(paginatedChannelViewData); + }) + .catch(error => { + reject(error); + }); + }); + }, + getLocalFileRecord (claimId, name) { + return db.File.findOne({where: {claimId, name}}) + .then(file => { + if (!file) { + return NO_FILE; + } + return file.dataValues; + }); + }, }; diff --git a/controllers/statsController.js b/controllers/statsController.js index 6f3d4475..68215767 100644 --- a/controllers/statsController.js +++ b/controllers/statsController.js @@ -70,25 +70,23 @@ module.exports = { }); }, getTrendingClaims (startDate) { - logger.debug('retrieving trending requests'); + logger.debug('retrieving trending'); return new Promise((resolve, reject) => { // get the raw requests data - db.getTrendingClaims(startDate) - .then(results => { - if (results) { - results.forEach(element => { - const fileExtenstion = element.fileType.substring(element.fileType.lastIndexOf('/') + 1); - element['showUrlLong'] = `/${element.claimId}/${element.name}`; - element['directUrlLong'] = `/${element.claimId}/${element.name}.${fileExtenstion}`; - element['directUrlShort'] = `/${element.claimId}/${element.name}.${fileExtenstion}`; - element['contentType'] = element.fileType; - element['thumbnail'] = 'https://spee.ch/assets/img/video_thumb_default.png'; + db.getTrendingFiles(startDate) + .then(fileArray => { + let claimsPromiseArray = []; + if (fileArray) { + fileArray.forEach(file => { + claimsPromiseArray.push(db.Claim.resolveClaim(file.name, file.claimId)); }); + return Promise.all(claimsPromiseArray); } - resolve(results); + }) + .then(claimsArray => { + resolve(claimsArray); }) .catch(error => { - logger.error('sequelize error >>', error); reject(error); }); }); diff --git a/helpers/channelPagination.js b/helpers/channelPagination.js new file mode 100644 index 00000000..d454a9ff --- /dev/null +++ b/helpers/channelPagination.js @@ -0,0 +1,71 @@ +const CLAIMS_PER_PAGE = 10; + +module.exports = { + returnPaginatedChannelViewData (channelName, longChannelClaimId, shortChannelClaimId, claims, query) { + const totalPages = module.exports.determineTotalPages(claims); + const paginationPage = module.exports.getPageFromQuery(query); + const viewData = { + channelName : channelName, + longChannelClaimId : longChannelClaimId, + shortChannelClaimId: shortChannelClaimId, + claims : module.exports.extractPageFromClaims(claims, paginationPage), + previousPage : module.exports.determinePreviousPage(paginationPage), + currentPage : paginationPage, + nextPage : module.exports.determineNextPage(totalPages, paginationPage), + totalPages : totalPages, + totalResults : module.exports.determineTotalClaims(claims), + }; + return viewData; + }, + getPageFromQuery (query) { + if (query.p) { + return parseInt(query.p); + } + return 1; + }, + extractPageFromClaims (claims, pageNumber) { + if (!claims) { + return []; // if no claims, return this default + } + // logger.debug('claims is array?', Array.isArray(claims)); + // logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber)); + const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE; + const claimEndIndex = claimStartIndex + 10; + const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex); + return pageOfClaims; + }, + determineTotalPages (claims) { + if (!claims) { + return 0; + } else { + const totalClaims = claims.length; + if (totalClaims < CLAIMS_PER_PAGE) { + return 1; + } + const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE); + const remainder = totalClaims % CLAIMS_PER_PAGE; + if (remainder === 0) { + return fullPages; + } + return fullPages + 1; + } + }, + determinePreviousPage (currentPage) { + if (currentPage === 1) { + return null; + } + return currentPage - 1; + }, + determineNextPage (totalPages, currentPage) { + if (currentPage === totalPages) { + return null; + } + return currentPage + 1; + }, + determineTotalClaims (claims) { + if (!claims) { + return 0; + } + return claims.length; + }, +}; diff --git a/helpers/errorHandlers.js b/helpers/errorHandlers.js index aa1f723d..4a825399 100644 --- a/helpers/errorHandlers.js +++ b/helpers/errorHandlers.js @@ -1,5 +1,4 @@ const logger = require('winston'); -const { postToStats } = require('../controllers/statsController.js'); module.exports = { returnErrorMessageAndStatus: function (error) { @@ -33,17 +32,15 @@ module.exports = { } return [status, message]; }, - handleRequestError: function (action, originalUrl, ip, error, res) { + handleRequestError: function (originalUrl, ip, error, res) { logger.error(`Request Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); - postToStats(action, originalUrl, ip, null, null, error); const [status, message] = module.exports.returnErrorMessageAndStatus(error); res .status(status) .render('requestError', module.exports.createErrorResponsePayload(status, message)); }, - handleApiError: function (action, originalUrl, ip, error, res) { - logger.error(`Api ${action} Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); - postToStats(action, originalUrl, ip, null, null, error); + handleApiError: function (originalUrl, ip, error, res) { + logger.error(`Api Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); const [status, message] = module.exports.returnErrorMessageAndStatus(error); res .status(status) diff --git a/helpers/handlebarsHelpers.js b/helpers/handlebarsHelpers.js index 59275e0c..d2258e03 100644 --- a/helpers/handlebarsHelpers.js +++ b/helpers/handlebarsHelpers.js @@ -2,6 +2,10 @@ const Handlebars = require('handlebars'); const config = require('../config/speechConfig.js'); module.exports = { + placeCommonHeaderTags () { + const headerBoilerplate = `Spee.ch`; + return new Handlebars.SafeString(headerBoilerplate); + }, googleAnalytics () { const googleApiKey = config.analytics.googleId; const gaCode = ``; return new Handlebars.SafeString(gaCode); }, - addOpenGraph (title, mimeType, showUrl, source, description, thumbnail) { - if (title === null || title.trim() === '') { - title = 'Spee.ch'; - } - if (description === null || description.trim() === '') { - description = 'Open-source, decentralized image and video sharing.'; - } - const ogTitle = ``; - const ogUrl = ``; - const ogSiteName = ``; - const ogDescription = ``; - const ogImageWidth = ''; - const ogImageHeight = ''; - const basicTags = `${ogTitle} ${ogUrl} ${ogSiteName} ${ogDescription} ${ogImageWidth} ${ogImageHeight}`; - let ogImage = ``; - let ogImageType = ``; - let ogType = ``; - if (mimeType === 'video/mp4') { - const ogVideo = ``; - const ogVideoSecureUrl = ``; - const ogVideoType = ``; - ogImage = ``; - ogImageType = ``; - ogType = ``; - return new Handlebars.SafeString(`${basicTags} ${ogImage} ${ogImageType} ${ogType} ${ogVideo} ${ogVideoSecureUrl} ${ogVideoType}`); + addOpenGraph ({ ogTitle, contentType, ogDescription, thumbnail, showUrl, source, ogThumbnailContentType }) { + const ogTitleTag = ``; + const ogUrlTag = ``; + const ogSiteNameTag = ``; + const ogDescriptionTag = ``; + const ogImageWidthTag = ''; + const ogImageHeightTag = ''; + const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`; + let ogImageTag = ``; + let ogImageTypeTag = ``; + let ogTypeTag = ``; + if (contentType === 'video/mp4') { + const ogVideoTag = ``; + const ogVideoSecureUrlTag = ``; + const ogVideoTypeTag = ``; + ogImageTag = ``; + ogImageTypeTag = ``; + ogTypeTag = ``; + return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`); } else { - if (mimeType === 'image/gif') { - ogType = ``; + if (contentType === 'image/gif') { + ogTypeTag = ``; }; - return new Handlebars.SafeString(`${basicTags} ${ogImage} ${ogImageType} ${ogType}`); + return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag}`); } }, - addTwitterCard (mimeType, source, embedUrl, directFileUrl) { + addTwitterCard ({ contentType, source, embedUrl, directFileUrl }) { const basicTwitterTags = ``; - if (mimeType === 'video/mp4') { + if (contentType === 'video/mp4') { const twitterName = ''; const twitterPlayer = ``; const twitterPlayerWidth = ''; diff --git a/helpers/lbryApi.js b/helpers/lbryApi.js index bfd3139c..0474ca36 100644 --- a/helpers/lbryApi.js +++ b/helpers/lbryApi.js @@ -10,7 +10,6 @@ function handleLbrynetResponse ({ data }, resolve, reject) { reject(data.result.error); return; }; - // logger.debug('data.result', data.result); resolve(data.result); return; } diff --git a/helpers/lbryUri.js b/helpers/lbryUri.js new file mode 100644 index 00000000..cc5e1d9e --- /dev/null +++ b/helpers/lbryUri.js @@ -0,0 +1,90 @@ +const logger = require('winston'); + +module.exports = { + REGEXP_INVALID_CLAIM : /[^A-Za-z0-9-]/g, + REGEXP_INVALID_CHANNEL: /[^A-Za-z0-9-@]/g, + REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/, + CHANNEL_CHAR : '@', + parseIdentifier : function (identifier) { + logger.debug('parsing identifier:', identifier); + const componentsRegex = new RegExp( + '([^:$#/]*)' + // value (stops at the first separator or end) + '([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end) + ); + const [proto, value, modifierSeperator, modifier] = componentsRegex + .exec(identifier) + .map(match => match || null); + logger.debug(`${proto}, ${value}, ${modifierSeperator}, ${modifier}`); + + // Validate and process name + const isChannel = value.startsWith(module.exports.CHANNEL_CHAR); + const channelName = isChannel ? value : null; + let claimId; + if (isChannel) { + if (!channelName) { + throw new Error('No channel name after @.'); + } + const nameBadChars = (channelName).match(module.exports.REGEXP_INVALID_CHANNEL); + if (nameBadChars) { + throw new Error(`Invalid characters in channel name: ${nameBadChars.join(', ')}.`); + } + } else { + claimId = value; + } + + // Validate and process modifier + let channelClaimId; + if (modifierSeperator) { + if (!modifier) { + throw new Error(`No modifier provided after separator ${modifierSeperator}.`); + } + + if (modifierSeperator === ':') { + channelClaimId = modifier; + } else { + throw new Error(`The ${modifierSeperator} modifier is not currently supported.`); + } + } + return { + isChannel, + channelName, + channelClaimId, + claimId, + }; + }, + parseName: function (name) { + logger.debug('parsing name:', name); + const componentsRegex = new RegExp( + '([^:$#/.]*)' + // name (stops at the first modifier) + '([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end) + ); + const [proto, claimName, modifierSeperator, modifier] = componentsRegex + .exec(name) + .map(match => match || null); + logger.debug(`${proto}, ${claimName}, ${modifierSeperator}, ${modifier}`); + + // Validate and process name + if (!claimName) { + throw new Error('No claim name provided before .'); + } + const nameBadChars = (claimName).match(module.exports.REGEXP_INVALID_CLAIM); + if (nameBadChars) { + throw new Error(`Invalid characters in claim name: ${nameBadChars.join(', ')}.`); + } + // Validate and process modifier + let isServeRequest = false; + if (modifierSeperator) { + if (!modifier) { + throw new Error(`No file extension provided after separator ${modifierSeperator}.`); + } + if (modifierSeperator !== '.') { + throw new Error(`The ${modifierSeperator} modifier is not supported in the claim name`); + } + isServeRequest = true; + } + return { + claimName, + isServeRequest, + }; + }, +}; diff --git a/helpers/sequelizeHelpers.js b/helpers/sequelizeHelpers.js index 3b36749d..0297f676 100644 --- a/helpers/sequelizeHelpers.js +++ b/helpers/sequelizeHelpers.js @@ -1,23 +1,23 @@ module.exports = { - returnShortId: function (result, longId) { + returnShortId: function (claimsArray, longId) { let claimIndex; - let shortId = longId.substring(0, 1); // default sort id is the first letter + let shortId = longId.substring(0, 1); // default short id is the first letter let shortIdLength = 0; // find the index of this claim id - claimIndex = result.findIndex(element => { + claimIndex = claimsArray.findIndex(element => { return element.claimId === longId; }); if (claimIndex < 0) { throw new Error('claim id not found in claims list'); } // get an array of all claims with lower height - let possibleMatches = result.slice(0, claimIndex); + let possibleMatches = claimsArray.slice(0, claimIndex); // remove certificates with the same prefixes until none are left. while (possibleMatches.length > 0) { shortIdLength += 1; shortId = longId.substring(0, shortIdLength); possibleMatches = possibleMatches.filter(element => { - return (element.claimId.substring(0, shortIdLength) === shortId); + return (element.claimId && (element.claimId.substring(0, shortIdLength) === shortId)); }); } return shortId; diff --git a/helpers/serveHelpers.js b/helpers/serveHelpers.js index 6e7c610a..352e4748 100644 --- a/helpers/serveHelpers.js +++ b/helpers/serveHelpers.js @@ -1,45 +1,23 @@ const logger = require('winston'); -function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) { - return { - embedUrl : `https://spee.ch/embed/${claimId}/${name}`, - showUrl : `https://spee.ch/${claimId}/${name}`, - source : `https://spee.ch/${claimId}/${name}.${fileExt}`, - directFileUrl: `https://spee.ch/${claimId}/${name}.${fileExt}`, - }; -} - module.exports = { - serveFile ({ fileName, fileType, filePath }, res) { - logger.verbose(`serving file ${fileName}`); - // set default options - let options = { + serveFile ({ filePath, fileType }, claimId, name, res) { + logger.verbose(`serving ${name}#${claimId}`); + // set response options + const headerContentType = fileType || 'image/jpeg'; + const options = { headers: { 'X-Content-Type-Options': 'nosniff', - 'Content-Type' : fileType, + 'Content-Type' : headerContentType, }, }; - // adjust default options as needed - switch (fileType) { - case 'image/jpeg': - case 'image/gif': - case 'image/png': - case 'video/mp4': - break; - default: - logger.warn('sending file with unknown type as .jpeg'); - options['headers']['Content-Type'] = 'image/jpeg'; - break; - } // send the file res.status(200).sendFile(filePath, options); }, - showFile (fileInfo, res) { - const openGraphInfo = createOpenGraphInfo(fileInfo); - res.status(200).render('show', { layout: 'show', fileInfo, openGraphInfo }); + showFile (claimInfo, shortId, res) { + res.status(200).render('show', { layout: 'show', claimInfo, shortId }); }, - showFileLite (fileInfo, res) { - const openGraphInfo = createOpenGraphInfo(fileInfo); - res.status(200).render('showLite', { layout: 'showlite', fileInfo, openGraphInfo }); + showFileLite (claimInfo, shortId, res) { + res.status(200).render('showLite', { layout: 'showlite', claimInfo, shortId }); }, }; diff --git a/models/certificate.js b/models/certificate.js index 37dafbaa..206b3e40 100644 --- a/models/certificate.js +++ b/models/certificate.js @@ -1,6 +1,5 @@ const logger = require('winston'); const { returnShortId } = require('../helpers/sequelizeHelpers.js'); -const NO_CHANNEL = 'NO_CHANNEL'; module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { const Certificate = sequelize.define( @@ -123,14 +122,14 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; - Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelId) { + Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelClaimId) { return new Promise((resolve, reject) => { this .findAll({ where: { name : channelName, claimId: { - $like: `${channelId}%`, + $like: `${channelClaimId}%`, }, }, order: [['height', 'ASC']], @@ -138,7 +137,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { .then(result => { switch (result.length) { case 0: - return resolve(NO_CHANNEL); + return resolve(null); default: // note results must be sorted return resolve(result[0].claimId); } @@ -160,7 +159,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { .then(result => { switch (result.length) { case 0: - return resolve(NO_CHANNEL); + return resolve(null); default: return resolve(result[0].claimId); } @@ -171,12 +170,29 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; - Certificate.getLongChannelId = function (channelName, channelId) { - logger.debug(`getLongChannelId(${channelName}, ${channelId})`); - if (channelId && (channelId.length === 40)) { // if a full channel id is provided - return new Promise((resolve, reject) => resolve(channelId)); - } else if (channelId && channelId.length < 40) { // if a short channel id is provided - return this.getLongChannelIdFromShortChannelId(channelName, channelId); + Certificate.validateLongChannelId = function (name, claimId) { + return new Promise((resolve, reject) => { + this.findOne({ + where: {name, claimId}, + }) + .then(result => { + if (!result) { + return resolve(null); + }; + resolve(claimId); + }) + .catch(error => { + reject(error); + }); + }); + }; + + Certificate.getLongChannelId = function (channelName, channelClaimId) { + logger.debug(`getLongChannelId(${channelName}, ${channelClaimId})`); + if (channelClaimId && (channelClaimId.length === 40)) { // if a full channel id is provided + return this.validateLongChannelId(channelName, channelClaimId); + } else if (channelClaimId && channelClaimId.length < 40) { // if a short channel id is provided + return this.getLongChannelIdFromShortChannelId(channelName, channelClaimId); } else { return this.getLongChannelIdFromChannelName(channelName); // if no channel id provided } diff --git a/models/claim.js b/models/claim.js index 0c7289fd..5ec31da7 100644 --- a/models/claim.js +++ b/models/claim.js @@ -1,6 +1,89 @@ const logger = require('winston'); const { returnShortId } = require('../helpers/sequelizeHelpers.js'); -const NO_CLAIM = 'NO_CLAIM'; +const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png'; +const DEFAULT_TITLE = 'Spee { const Claim = sequelize.define( @@ -159,7 +242,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }; Claim.getShortClaimIdFromLongClaimId = function (claimId, claimName) { - logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimId}#${claimId}`); + logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`); return new Promise((resolve, reject) => { this .findAll({ @@ -180,20 +263,27 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; - Claim.getAllChannelClaims = function (channelId) { - logger.debug(`Claim.getAllChannelClaims for ${channelId}`); + Claim.getAllChannelClaims = function (channelClaimId) { + logger.debug(`Claim.getAllChannelClaims for ${channelClaimId}`); return new Promise((resolve, reject) => { this .findAll({ - where: { certificateId: channelId }, + where: { certificateId: channelClaimId }, order: [['height', 'ASC']], + raw : true, // returns an array of only data, not an array of instances }) - .then(result => { - switch (result.length) { + .then(channelClaimsArray => { + // logger.debug('channelclaimsarray length:', channelClaimsArray.length); + switch (channelClaimsArray.length) { case 0: return resolve(null); default: - return resolve(result); + channelClaimsArray.forEach(claim => { + claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType); + claim['thumbnail'] = determineThumbnail(claim.thumbnail, DEFAULT_THUMBNAIL); + return claim; + }); + return resolve(channelClaimsArray); } }) .catch(error => { @@ -202,18 +292,18 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; - Claim.getClaimIdByLongChannelId = function (channelId, claimName) { - logger.debug(`finding claim id for claim ${claimName} from channel ${channelId}`); + Claim.getClaimIdByLongChannelId = function (channelClaimId, claimName) { + logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`); return new Promise((resolve, reject) => { this .findAll({ - where: { name: claimName, certificateId: channelId }, + where: { name: claimName, certificateId: channelClaimId }, order: [['id', 'ASC']], }) .then(result => { switch (result.length) { case 0: - return resolve(NO_CLAIM); + return resolve(null); case 1: return resolve(result[0].claimId); default: @@ -241,7 +331,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { .then(result => { switch (result.length) { case 0: - return resolve(NO_CLAIM); + return resolve(null); default: // note results must be sorted return resolve(result[0].claimId); } @@ -260,12 +350,12 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { order: [['effectiveAmount', 'DESC'], ['height', 'ASC']], // note: maybe height and effective amount need to switch? }) .then(result => { + logger.debug('length of result', result.length); switch (result.length) { case 0: - return resolve(NO_CLAIM); + return resolve(null); default: - logger.debug('getTopFreeClaimIdByClaimName result:', result.dataValues); - return resolve(result[0].claimId); + return resolve(result[0].dataValues.claimId); } }) .catch(error => { @@ -274,10 +364,27 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; + Claim.validateLongClaimId = function (name, claimId) { + return new Promise((resolve, reject) => { + this.findOne({ + where: {name, claimId}, + }) + .then(result => { + if (!result) { + return resolve(null); + }; + resolve(claimId); + }) + .catch(error => { + reject(error); + }); + }); + }; + Claim.getLongClaimId = function (claimName, claimId) { logger.debug(`getLongClaimId(${claimName}, ${claimId})`); if (claimId && (claimId.length === 40)) { // if a full claim id is provided - return new Promise((resolve, reject) => resolve(claimId)); + return this.validateLongClaimId(claimName, claimId); } else if (claimId && claimId.length < 40) { return this.getLongClaimIdFromShortClaimId(claimName, claimId); // if a short claim id is provided } else { @@ -291,15 +398,16 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { .findAll({ where: { name, claimId }, }) - .then(result => { - switch (result.length) { + .then(claimArray => { + logger.debug('claims found on resolve:', claimArray.length); + switch (claimArray.length) { case 0: return resolve(null); case 1: - return resolve(result[0]); + return resolve(prepareClaimData(claimArray[0].dataValues)); default: - logger.warn(`more than one entry matches that name (${name}) and claimID (${claimId})`); - return resolve(result[0]); + logger.error(`more than one entry matches that name (${name}) and claimID (${claimId})`); + return resolve(prepareClaimData(claimArray[0].dataValues)); } }) .catch(error => { diff --git a/models/index.js b/models/index.js index 4bc622fd..b959586c 100644 --- a/models/index.js +++ b/models/index.js @@ -69,11 +69,12 @@ db.upsert = (Model, values, condition, tableName) => { }) .catch(function (error) { logger.error(`${tableName}.upsert error`, error); + throw error; }); }; -// add a 'getTrendingClaims' method to the db object -db.getTrendingClaims = (startDate) => { +// add a 'getTrendingFiles' method to the db object. note: change this to get claims directly. might need new association between Request and Claim +db.getTrendingFiles = (startDate) => { return db.sequelize.query(`SELECT COUNT(*), File.* FROM Request LEFT JOIN File ON Request.FileId = File.id WHERE FileId IS NOT NULL AND nsfw != 1 AND trendingEligible = 1 AND Request.createdAt > "${startDate}" GROUP BY FileId ORDER BY COUNT(*) DESC LIMIT 25;`, { type: db.sequelize.QueryTypes.SELECT }); }; diff --git a/public/assets/css/general.css b/public/assets/css/general.css index 8ef7376b..57c26354 100644 --- a/public/assets/css/general.css +++ b/public/assets/css/general.css @@ -99,6 +99,10 @@ h3, p { font-size: small; } +#show-body > .fine-print { + text-align: center; +} + .blue { color: #4156C5; } @@ -153,9 +157,7 @@ a, a:visited { .link--primary, .link--primary:visited { color: #4156C5; } -.link--primary.fine-print { - text-align: center; -} + .link--nav { color: black; border-bottom: 2px solid white; @@ -296,10 +298,6 @@ a, a:visited { color: red; } -.info-message-placeholder { - -} - /* INPUT FIELDS */ /* blocks */ @@ -347,14 +345,6 @@ option { cursor: pointer; } -#claim-name-input { - -} - -#input-success-claim-name { - -} - .span--relative { position: relative; } @@ -511,27 +501,34 @@ table { width: calc(100% - 1rem); } -/* Show page */ +/* Assets */ -.video-show, .gifv-show, .image-show { - display: block; - width: 100%; +.asset { + max-width: 100%; } -#video-player, .showlite-asset { - display: block; - margin: 0 auto; - background-color: #fff; + +#show-body #asset-boilerpate { + display: none; +} + +#showlite-body #asset-display-component { + max-width: 50%; + text-align: center; +} + +/* video */ + +#video-asset { + background-color: #000000; cursor: pointer; } -#showlite-body #video-player { - margin-top: 2%; - padding: 6px; - max-width: 50%; +#showlite-body #video-asset { + background-color: #ffffff; + width: calc(100% - 12px - 12px - 2px); + margin: 6px; + padding: 6px; border: 1px solid #d0d0d0; } -.showlite-asset { - max-width: 100%; -} /* item lists */ diff --git a/public/assets/css/mediaQueries.css b/public/assets/css/mediaQueries.css index 03ecbfdd..52eca278 100644 --- a/public/assets/css/mediaQueries.css +++ b/public/assets/css/mediaQueries.css @@ -36,10 +36,14 @@ padding-right: 1.5em; } - .showlite-asset { + #showlite-body #asset-display-component { max-width: 100%; } + #showlite-body #asset-status { + padding: 2em; + } + } @media (max-width: 500px) { diff --git a/public/assets/js/assetConstructor.js b/public/assets/js/assetConstructor.js new file mode 100644 index 00000000..bb001b0d --- /dev/null +++ b/public/assets/js/assetConstructor.js @@ -0,0 +1,139 @@ +const Asset = function () { + this.data = {}; + this.addPlayPauseToVideo = function () { + const that = this; + const video = document.getElementById('video-asset'); + if (video) { + // add event listener for click + video.addEventListener('click', ()=> { + that.playOrPause(video); + }); + // add event listener for space bar + document.body.onkeyup = (event) => { + if (event.keyCode == 32) { + that.playOrPause(video); + } + }; + } + }; + this.playOrPause = function(video){ + if (video.paused == true) { + video.play(); + } + else{ + video.pause(); + } + }; + this.showAsset = function () { + this.hideAssetStatus(); + this.showAssetHolder(); + if (!this.data.src) { + return console.log('error: src is not set') + } + if (!this.data.contentType) { + return console.log('error: contentType is not set') + } + if (this.data.contentType === 'video/mp4') { + this.showVideo(); + } else { + this.showImage(); + } + }; + this.showVideo = function () { + console.log('showing video', this.data.src); + const video = document.getElementById('video-asset'); + const source = document.createElement('source'); + source.setAttribute('src', this.data.src); + video.appendChild(source); + video.play(); + }; + this.showImage = function () { + console.log('showing image', this.data.src); + const asset = document.getElementById('image-asset'); + asset.setAttribute('src', this.data.src); + }; + this.hideAssetStatus = function () { + const assetStatus = document.getElementById('asset-status'); + assetStatus.hidden = true; + }; + this.showAssetHolder =function () { + const assetHolder = document.getElementById('asset-holder'); + assetHolder.hidden = false; + }; + this.showSearchMessage = function () { + const searchMessage = document.getElementById('searching-message'); + searchMessage.hidden = false; + }; + this.showFailureMessage = function (msg) { + console.log(msg); + const searchMessage = document.getElementById('searching-message'); + const failureMessage = document.getElementById('failure-message'); + const errorMessage = document.getElementById('error-message'); + searchMessage.hidden = true; + failureMessage.hidden = false; + errorMessage.innerText = msg; + }; + this.checkFileAndRenderAsset = function () { + const that = this; + this.isFileAvailable() + .then(isAvailable => { + if (!isAvailable) { + console.log('file is not yet available on spee.ch'); + that.showSearchMessage(); + return that.getAssetOnSpeech(); + } + }) + .then(() => { + that.showAsset(); + }) + .catch(error => { + that.showFailureMessage(error); + }) + }; + this.isFileAvailable = function () { + console.log(`checking if file is available for ${this.data.claimName}#${this.data.claimId}`) + const uri = `/api/file-is-available/${this.data.claimName}/${this.data.claimId}`; + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + xhr.open("GET", uri, true); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + const response = JSON.parse(xhr.response); + if (xhr.status == 200) { + console.log('isFileAvailable succeeded:', response); + if (response.message === true) { + resolve(true); + } else { + resolve(false); + } + } else { + console.log('isFileAvailable failed:', response); + reject('Well this sucks, but we can\'t seem to phone home'); + } + } + }; + xhr.send(); + }) + }; + this.getAssetOnSpeech = function() { + console.log(`getting claim for ${this.data.claimName}#${this.data.claimId}`) + const uri = `/api/claim-get/${this.data.claimName}/${this.data.claimId}`; + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + xhr.open("GET", uri, true); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + const response = JSON.parse(xhr.response); + if (xhr.status == 200) { + console.log('getAssetOnSpeech succeeded:', response) + resolve(true); + } else { + console.log('getAssetOnSpeech failed:', response); + reject(response.message); + } + } + }; + xhr.send(); + }) + }; +}; \ No newline at end of file diff --git a/public/assets/js/progressBarConstructor.js b/public/assets/js/progressBarConstructor.js new file mode 100644 index 00000000..468cf178 --- /dev/null +++ b/public/assets/js/progressBarConstructor.js @@ -0,0 +1,45 @@ +const ProgressBar = function() { + this.data = { + x: 0, + adder: 1, + bars: [], + }; + this.barHolder = document.getElementById('bar-holder'); + this.createProgressBar = function (size) { + this.data['size'] = size; + for (var i = 0; i < size; i++) { + const bar = document.createElement('span'); + bar.innerText = '| '; + bar.setAttribute('class', 'progress-bar progress-bar--inactive'); + this.barHolder.appendChild(bar); + this.data.bars.push(bar); + } + }; + this.startProgressBar = function () { + this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300); + }; + this.updateProgressBar = function () { + const x = this.data.x; + const adder = this.data.adder; + const size = this.data.size; + // update the appropriate bar + if (x > -1 && x < size){ + if (adder === 1){ + this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--active'); + } else { + this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--inactive'); + } + } + // update adder + if (x === size){ + this.data['adder'] = -1; + } else if ( x === -1){ + this.data['adder'] = 1; + } + // update x + this.data['x'] = x + adder; + }; + this.stopProgressBar = function () { + clearInterval(this.updateInterval); + }; +}; \ No newline at end of file diff --git a/public/assets/js/publishFileFunctions.js b/public/assets/js/publishFileFunctions.js index 19c273ab..1f552bb8 100644 --- a/public/assets/js/publishFileFunctions.js +++ b/public/assets/js/publishFileFunctions.js @@ -102,7 +102,7 @@ const publishFileFunctions = { return fd; }, publishFile: function (file, metadata) { - var uri = "/api/publish"; + var uri = "/api/claim-publish"; var xhr = new XMLHttpRequest(); var fd = this.appendDataToFormData(file, metadata); var that = this; diff --git a/public/assets/js/showFunctions.js b/public/assets/js/showFunctions.js deleted file mode 100644 index 94047cd3..00000000 --- a/public/assets/js/showFunctions.js +++ /dev/null @@ -1,23 +0,0 @@ -function playOrPause(video){ - if (video.paused == true) { - video.play(); - } - else{ - video.pause(); - } -} - -// if a video player is present, set the listeners -const video = document.getElementById('video-player'); -if (video) { - // add event listener for click - video.addEventListener('click', ()=> { - playOrPause(video); - }); - // add event listener for space bar - document.body.onkeyup = (event) => { - if (event.keyCode == 32) { - playOrPause(video); - } - }; -} \ No newline at end of file diff --git a/public/assets/js/validationFunctions.js b/public/assets/js/validationFunctions.js index f79cc4fa..85d97868 100644 --- a/public/assets/js/validationFunctions.js +++ b/public/assets/js/validationFunctions.js @@ -119,13 +119,13 @@ const validationFunctions = { checkClaimName: function (name) { const successDisplayElement = document.getElementById('input-success-claim-name'); const errorDisplayElement = document.getElementById('input-error-claim-name'); - this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/isClaimAvailable/'); + this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/claim-is-available/'); }, checkChannelName: function (name) { const successDisplayElement = document.getElementById('input-success-channel-name'); const errorDisplayElement = document.getElementById('input-error-channel-name'); name = `@${name}`; - this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/isChannelAvailable/'); + this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/'); }, // validation function which checks all aspects of the publish submission validateFilePublishSubmission: function (stagedFiles, metadata) { @@ -162,7 +162,7 @@ const validationFunctions = { return; } // if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?) - return that.isNameAvailable(claimName, '/api/isClaimAvailable/') + return that.isNameAvailable(claimName, '/api/claim-is-available/') .then(result => { if (result) { resolve(); @@ -193,7 +193,7 @@ const validationFunctions = { return reject(error); } // 3. if all validation passes, check availability of the name - that.isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability + that.isNameAvailable(channelName, '/api/channel-is-available/') // validate the availability .then(function(result) { if (result) { resolve(); diff --git a/routes/api-routes.js b/routes/api-routes.js index 38b5b1e8..95bf222b 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -4,36 +4,93 @@ const config = require('../config/speechConfig.js'); const multipartMiddleware = multipart({uploadDir: config.files.uploadDirectory}); const db = require('../models'); const { publish } = require('../controllers/publishController.js'); -const { getClaimList, resolveUri } = require('../helpers/lbryApi.js'); +const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js'); const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseChannelName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js'); const errorHandlers = require('../helpers/errorHandlers.js'); -const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js'); const { authenticateOrSkip } = require('../auth/authentication.js'); +function addGetResultsToFileData (fileInfo, getResult) { + fileInfo.fileName = getResult.file_name; + fileInfo.filePath = getResult.download_path; + return fileInfo; +} + +function createFileData ({ name, claimId, outpoint, height, address, nsfw, contentType }) { + return { + name, + claimId, + outpoint, + height, + address, + fileName: '', + filePath: '', + fileType: contentType, + nsfw, + }; +} + module.exports = (app) => { // route to run a claim_list request on the daemon - app.get('/api/claim_list/:name', ({ headers, ip, originalUrl, params }, res) => { - // google analytics - sendGoogleAnalytics('SERVE', headers, ip, originalUrl); - // serve the content + app.get('/api/claim-list/:name', ({ ip, originalUrl, params }, res) => { getClaimList(params.name) .then(claimsList => { - postToStats('serve', originalUrl, ip, null, null, 'success'); res.status(200).json(claimsList); }) .catch(error => { - errorHandlers.handleApiError('claim_list', originalUrl, ip, error, res); + errorHandlers.handleApiError(originalUrl, ip, error, res); }); }); + // route to see if asset is available locally + app.get('/api/file-is-available/:name/:claimId', ({ ip, originalUrl, params }, res) => { + const name = params.name; + const claimId = params.claimId; + let isLocalFileAvailable = false; + db.File.findOne({where: {name, claimId}}) + .then(result => { + if (result) { + isLocalFileAvailable = true; + } + res.status(200).json({status: 'success', message: isLocalFileAvailable}); + }) + .catch(error => { + errorHandlers.handleApiError(originalUrl, ip, error, res); + }); + }); + // route to get an asset + app.get('/api/claim-get/:name/:claimId', ({ ip, originalUrl, params }, res) => { + const name = params.name; + const claimId = params.claimId; + // resolve the claim + db.Claim.resolveClaim(name, claimId) + .then(resolveResult => { + // make sure a claim actually exists at that uri + if (!resolveResult) { + throw new Error('No matching uri found in Claim table'); + } + let fileData = createFileData(resolveResult); + // get the claim + return Promise.all([fileData, getClaim(`${name}#${claimId}`)]); + }) + .then(([ fileData, getResult ]) => { + fileData = addGetResultsToFileData(fileData, getResult); + return Promise.all([db.upsert(db.File, fileData, {name, claimId}, 'File'), getResult]); + }) + .then(([ fileRecord, {message, completed} ]) => { + res.status(200).json({ status: 'success', message, completed }); + }) + .catch(error => { + errorHandlers.handleApiError(originalUrl, ip, error, res); + }); + }); + // route to check whether spee.ch has published to a claim - app.get('/api/isClaimAvailable/:name', ({ params }, res) => { - // send response + app.get('/api/claim-is-available/:name', ({ params }, res) => { checkClaimNameAvailability(params.name) .then(result => { if (result === true) { res.status(200).json(true); } else { - logger.debug(`Rejecting '${params.name}' because that name has already been claimed on spee.ch`); + // logger.debug(`Rejecting '${params.name}' because that name has already been claimed on spee.ch`); res.status(200).json(false); } }) @@ -42,37 +99,32 @@ module.exports = (app) => { }); }); // route to check whether spee.ch has published to a channel - app.get('/api/isChannelAvailable/:name', ({ params }, res) => { + app.get('/api/channel-is-available/:name', ({ params }, res) => { checkChannelAvailability(params.name) .then(result => { if (result === true) { res.status(200).json(true); } else { - logger.debug(`Rejecting '${params.name}' because that channel has already been claimed on spee.ch`); + // logger.debug(`Rejecting '${params.name}' because that channel has already been claimed on spee.ch`); res.status(200).json(false); } }) .catch(error => { - logger.debug('api/isChannelAvailable/ error', error); res.status(500).json(error); }); }); // route to run a resolve request on the daemon - app.get('/api/resolve/:uri', ({ headers, ip, originalUrl, params }, res) => { - // google analytics - sendGoogleAnalytics('SERVE', headers, ip, originalUrl); - // serve content + app.get('/api/claim-resolve/:uri', ({ headers, ip, originalUrl, params }, res) => { resolveUri(params.uri) .then(resolvedUri => { - postToStats('serve', originalUrl, ip, null, null, 'success'); res.status(200).json(resolvedUri); }) .catch(error => { - errorHandlers.handleApiError('resolve', originalUrl, ip, error, res); + errorHandlers.handleApiError(originalUrl, ip, error, res); }); }); // route to run a publish request on the daemon - app.post('/api/publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { + app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { let file, fileName, filePath, fileType, name, nsfw, license, title, description, thumbnail, anonymous, skipAuth, channelName, channelPassword; // validate that mandatory parts of the request are present try { @@ -123,7 +175,7 @@ module.exports = (app) => { } } channelName = cleanseChannelName(channelName); - logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); + logger.debug(`name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); // check channel authorization authenticateOrSkip(skipAuth, channelName, channelPassword) .then(authenticated => { @@ -156,13 +208,11 @@ module.exports = (app) => { }); }) .catch(error => { - errorHandlers.handleApiError('publish', originalUrl, ip, error, res); + errorHandlers.handleApiError(originalUrl, ip, error, res); }); }); - // route to get a short claim id from long claim Id - app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => { - // serve content + app.get('/api/claim-shorten-id/:longId/:name', ({ params }, res) => { db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name) .then(shortId => { res.status(200).json(shortId); @@ -173,8 +223,7 @@ module.exports = (app) => { }); }); // route to get a short channel id from long channel Id - app.get('/api/shortChannelId/:longId/:name', ({ ip, originalUrl, params }, res) => { - // serve content + app.get('/api/channel-shorten-id/:longId/:name', ({ ip, originalUrl, params }, res) => { db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name) .then(shortId => { logger.debug('sending back short channel id', shortId); @@ -182,7 +231,7 @@ module.exports = (app) => { }) .catch(error => { logger.error('api error getting short channel id', error); - errorHandlers.handleApiError('short channel id', originalUrl, ip, error, res); + errorHandlers.handleApiError(originalUrl, ip, error, res); }); }); }; diff --git a/routes/home-routes.js b/routes/home-routes.js index ed3472f5..0879194a 100644 --- a/routes/home-routes.js +++ b/routes/home-routes.js @@ -1,5 +1,3 @@ -const { postToStats } = require('../controllers/statsController.js'); - module.exports = app => { // route for the home page app.get('/', (req, res) => { @@ -7,8 +5,6 @@ module.exports = app => { }); // a catch-all route if someone visits a page that does not exist app.use('*', ({ originalUrl, ip }, res) => { - // post to stats - postToStats('show', originalUrl, ip, null, null, 'Error: 404'); // send response res.status(404).render('fourOhFour'); }); diff --git a/routes/page-routes.js b/routes/page-routes.js index ac56b889..450f55e0 100644 --- a/routes/page-routes.js +++ b/routes/page-routes.js @@ -30,24 +30,22 @@ module.exports = (app) => { const dateTime = startDate.toISOString().slice(0, 19).replace('T', ' '); getTrendingClaims(dateTime) .then(result => { - // logger.debug(result); res.status(200).render('popular', { trendingAssets: result, }); }) .catch(error => { - errorHandlers.handleRequestError('popular', originalUrl, ip, error, res); + errorHandlers.handleRequestError(originalUrl, ip, error, res); }); }); // route to display a list of the trending images app.get('/new', ({ ip, originalUrl }, res) => { getRecentClaims() .then(result => { - // logger.debug(result); res.status(200).render('new', { newClaims: result }); }) .catch(error => { - errorHandlers.handleRequestError('new', originalUrl, ip, error, res); + errorHandlers.handleRequestError(originalUrl, ip, error, res); }); }); // route to send embedable video player (for twitter) diff --git a/routes/serve-routes.js b/routes/serve-routes.js index eef33f93..cab39ed5 100644 --- a/routes/serve-routes.js +++ b/routes/serve-routes.js @@ -1,258 +1,205 @@ const logger = require('winston'); -const { getAssetByClaim, getChannelContents, getAssetByChannel, serveOrShowAsset } = require('../controllers/serveController.js'); +const { getClaimId, getChannelViewData, getLocalFileRecord } = require('../controllers/serveController.js'); +const serveHelpers = require('../helpers/serveHelpers.js'); const { handleRequestError } = require('../helpers/errorHandlers.js'); +const { postToStats } = require('../controllers/statsController.js'); +const db = require('../models'); +const lbryUri = require('../helpers/lbryUri.js'); const SERVE = 'SERVE'; const SHOW = 'SHOW'; const SHOWLITE = 'SHOWLITE'; -const CHANNEL = 'CHANNEL'; -const CLAIM = 'CLAIM'; -const CLAIM_ID_CHAR = ':'; -const CHANNEL_CHAR = '@'; -const CLAIMS_PER_PAGE = 10; const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; +const NO_FILE = 'NO_FILE'; function isValidClaimId (claimId) { return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId)); } function isValidShortId (claimId) { - return claimId.length === 1; // really it should evaluate the short url itself + return claimId.length === 1; // it should really evaluate the short url itself } function isValidShortIdOrClaimId (input) { return (isValidClaimId(input) || isValidShortId(input)); } -function getAsset (claimType, channelName, channelId, name, claimId) { - switch (claimType) { - case CHANNEL: - return getAssetByChannel(channelName, channelId, name); - case CLAIM: - return getAssetByClaim(name, claimId); +function sendChannelInfoAndContentToClient (channelPageData, res) { + if (channelPageData === NO_CHANNEL) { + res.status(200).render('noChannel'); + } else { + res.status(200).render('channel', channelPageData); + } +} + +function showChannelPageToClient (channelName, channelClaimId, originalUrl, ip, query, res) { + // 1. retrieve the channel contents + getChannelViewData(channelName, channelClaimId, query) + .then(channelViewData => { + sendChannelInfoAndContentToClient(channelViewData, res); + }) + .catch(error => { + handleRequestError(originalUrl, ip, error, res); + }); +} + +function clientAcceptsHtml (headers) { + return headers['accept'] && headers['accept'].split(',').includes('text/html'); +} + +function determineResponseType (isServeRequest, headers) { + let responseType; + if (isServeRequest) { + responseType = SERVE; + if (clientAcceptsHtml(headers)) { // this is in case a serve request comes from a browser + responseType = SHOWLITE; + } + } else { + responseType = SHOW; + if (!clientAcceptsHtml(headers)) { // this is in case someone embeds a show url + responseType = SERVE; + } + } + return responseType; +} + +function showAssetToClient (claimId, name, res) { + return Promise + .all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)]) + .then(([claimInfo, shortClaimId]) => { + logger.debug('claimInfo:', claimInfo); + logger.debug('shortClaimId:', shortClaimId); + return serveHelpers.showFile(claimInfo, shortClaimId, res); + }) + .catch(error => { + throw error; + }); +} + +function showLiteAssetToClient (claimId, name, res) { + return Promise + .all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)]) + .then(([claimInfo, shortClaimId]) => { + logger.debug('claimInfo:', claimInfo); + logger.debug('shortClaimId:', shortClaimId); + return serveHelpers.showFileLite(claimInfo, shortClaimId, res); + }) + .catch(error => { + throw error; + }); +} + +function serveAssetToClient (claimId, name, res) { + return getLocalFileRecord(claimId, name) + .then(fileInfo => { + logger.debug('fileInfo:', fileInfo); + if (fileInfo === NO_FILE) { + return res.status(307).redirect(`/api/claim-get/${name}/${claimId}`); + } + return serveHelpers.serveFile(fileInfo, claimId, name, res); + }) + .catch(error => { + throw error; + }); +} + +function showOrServeAsset (responseType, claimId, claimName, res) { + switch (responseType) { + case SHOW: + return showAssetToClient(claimId, claimName, res); + case SHOWLITE: + return showLiteAssetToClient(claimId, claimName, res); + case SERVE: + return serveAssetToClient(claimId, claimName, res); default: - return new Error('that claim type was not found'); + break; } } -function getPage (query) { - if (query.p) { - return parseInt(query.p); +function flipClaimNameAndIdForBackwardsCompatibility (identifier, name) { + // this is a patch for backwards compatability with 'spee.ch/name/claim_id' url format + if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) { + const tempName = name; + name = identifier; + identifier = tempName; } - return 1; + return [identifier, name]; } -function extractPageFromClaims (claims, pageNumber) { - logger.debug('claims is array?', Array.isArray(claims)); - logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber)); - const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE; - const claimEndIndex = claimStartIndex + 10; - const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex); - return pageOfClaims; -} - -function determineTotalPages (totalClaims) { - if (totalClaims === 0) { - return 0; - } - if (totalClaims < CLAIMS_PER_PAGE) { - return 1; - } - const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE); - const remainder = totalClaims % CLAIMS_PER_PAGE; - if (remainder === 0) { - return fullPages; - } - return fullPages + 1; -} - -function determinePreviousPage (currentPage) { - if (currentPage === 1) { - return null; - } - return currentPage - 1; -} - -function determineNextPage (totalPages, currentPage) { - if (currentPage === totalPages) { - return null; - } - return currentPage + 1; +function logRequestData (responseType, claimName, channelName, claimId) { + logger.debug('responseType ===', responseType); + logger.debug('claim name === ', claimName); + logger.debug('channel name ===', channelName); + logger.debug('claim id ===', claimId); } module.exports = (app) => { - // route to serve a specific asset + // route to serve a specific asset using the channel or claim id app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => { - let identifier = params.identifier; - let name = params.name; - let claimOrChannel; - let channelName = null; - let claimId = null; - let channelId = null; - let method; - let fileExtension; - // parse the name - const positionOfExtension = name.indexOf('.'); - if (positionOfExtension >= 0) { - fileExtension = name.substring(positionOfExtension + 1); - name = name.substring(0, positionOfExtension); - /* patch because twitter player preview adds '>' before file extension */ - if (name.indexOf('>') >= 0) { - name = name.substring(0, name.indexOf('>')); - } - /* end patch */ - logger.debug('file extension =', fileExtension); - if (headers['accept'] && headers['accept'].split(',').includes('text/html')) { - method = SHOWLITE; - } else { - method = SERVE; - } - } else { - method = SHOW; - if (!headers['accept'] || !headers['accept'].split(',').includes('text/html')) { - method = SERVE; - } + let isChannel, channelName, channelClaimId, claimId, claimName, isServeRequest; + try { + ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(params.identifier)); + ({ claimName, isServeRequest } = lbryUri.parseName(params.name)); + } catch (error) { + return handleRequestError(originalUrl, ip, error, res); } - /* patch for backwards compatability with spee.ch/name/claim_id */ - if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) { - let tempName = name; - name = identifier; - identifier = tempName; + if (!isChannel) { + [claimId, claimName] = flipClaimNameAndIdForBackwardsCompatibility(claimId, claimName); } - /* end patch */ - logger.debug('claim name =', name); - logger.debug('method =', method); - // parse identifier for whether it is a channel, short url, or claim_id - if (identifier.charAt(0) === '@') { - channelName = identifier; - claimOrChannel = CHANNEL; - const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR); - if (channelIdIndex !== -1) { - channelId = channelName.substring(channelIdIndex + 1); - channelName = channelName.substring(0, channelIdIndex); + let responseType = determineResponseType(isServeRequest, headers); + // log the request data for debugging + logRequestData(responseType, claimName, channelName, claimId); + // get the claim Id and then serve/show the asset + getClaimId(channelName, channelClaimId, claimName, claimId) + .then(fullClaimId => { + if (fullClaimId === NO_CLAIM) { + return res.status(200).render('noClaim'); + } else if (fullClaimId === NO_CHANNEL) { + return res.status(200).render('noChannel'); } - logger.debug('channel name =', channelName); - } else { - claimId = identifier; - logger.debug('claim id =', claimId); - claimOrChannel = CLAIM; - } - // 1. retrieve the asset and information - getAsset(claimOrChannel, channelName, channelId, name, claimId) - // 2. serve or show - .then(result => { - logger.debug('getAsset result:', result); - if (result === NO_CLAIM) { - res.status(200).render('noClaim'); - return; - } else if (result === NO_CHANNEL) { - res.status(200).render('noChannel'); - return; - } - return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res); - }) - // 3. update the file - .then(fileInfoForUpdate => { - // if needed, this is where we would update the file + showOrServeAsset(responseType, fullClaimId, claimName, res); + postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); }) .catch(error => { - handleRequestError('serve', originalUrl, ip, error, res); + handleRequestError(originalUrl, ip, error, res); }); }); - // route to serve the winning asset at a claim - app.get('/:name', ({ headers, ip, originalUrl, params, query }, res) => { - // parse name param - let name = params.name; - let method; - let fileExtension; - let channelName = null; - let channelId = null; - // (a) handle channel requests - if (name.charAt(0) === CHANNEL_CHAR) { - channelName = name; - const paginationPage = getPage(query); - const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR); - if (channelIdIndex !== -1) { - channelId = channelName.substring(channelIdIndex + 1); - channelName = channelName.substring(0, channelIdIndex); - } - logger.debug('channel name =', channelName); - logger.debug('channel Id =', channelId); - // 1. retrieve the channel contents - getChannelContents(channelName, channelId) - // 2. respond to the request - .then(result => { - if (result === NO_CHANNEL) { // no channel found - res.status(200).render('noChannel'); - } else if (!result.claims) { // channel found, but no claims - res.status(200).render('channel', { - layout : 'channel', - channelName : result.channelName, - longChannelId : result.longChannelId, - shortChannelId: result.shortChannelId, - claims : [], - previousPage : 0, - currentPage : 0, - nextPage : 0, - totalPages : 0, - totalResults : 0, - }); - } else { // channel found, with claims - const totalPages = determineTotalPages(result.claims.length); - res.status(200).render('channel', { - layout : 'channel', - channelName : result.channelName, - longChannelId : result.longChannelId, - shortChannelId: result.shortChannelId, - claims : extractPageFromClaims(result.claims, paginationPage), - previousPage : determinePreviousPage(paginationPage), - currentPage : paginationPage, - nextPage : determineNextPage(totalPages, paginationPage), - totalPages : totalPages, - totalResults : result.claims.length, - }); - } - }) - .catch(error => { - handleRequestError('serve', originalUrl, ip, error, res); - }); - // (b) handle stream requests + // route to serve the winning asset at a claim or a channel page + app.get('/:identifier', ({ headers, ip, originalUrl, params, query }, res) => { + let isChannel, channelName, channelClaimId; + try { + ({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(params.identifier)); + } catch (error) { + return handleRequestError(originalUrl, ip, error, res); + } + if (isChannel) { + // log the request data for debugging + logRequestData(null, null, channelName, null); + // handle showing the channel page + showChannelPageToClient(channelName, channelClaimId, originalUrl, ip, query, res); } else { - if (name.indexOf('.') !== -1) { - method = SERVE; - if (headers['accept'] && headers['accept'].split(',').includes('text/html')) { - method = SHOWLITE; - } - fileExtension = name.substring(name.indexOf('.') + 1); - name = name.substring(0, name.indexOf('.')); - logger.debug('file extension =', fileExtension); - } else { - method = SHOW; - if (!headers['accept'] || !headers['accept'].split(',').includes('text/html')) { - method = SERVE; - } + let claimName, isServeRequest; + try { + ({claimName, isServeRequest} = lbryUri.parseName(params.identifier)); + } catch (error) { + return handleRequestError(originalUrl, ip, error, res); } - logger.debug('claim name = ', name); - logger.debug('method =', method); - // 1. retrieve the asset and information - getAsset(CLAIM, null, null, name, null) - // 2. respond to the request - .then(result => { - logger.debug('getAsset result', result); - if (result === NO_CLAIM) { - res.status(200).render('noClaim'); - } else { - return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res); - } - }) - // 3. update the database - .then(fileInfoForUpdate => { - // if needed, this is where we would update the file - }) - .catch(error => { - handleRequestError('serve', originalUrl, ip, error, res); - }); + let responseType = determineResponseType(isServeRequest, headers); + // log the request data for debugging + logRequestData(responseType, claimName, null, null); + // get the claim Id and then serve/show the asset + getClaimId(null, null, claimName, null) + .then(fullClaimId => { + if (fullClaimId === NO_CLAIM) { + return res.status(200).render('noClaim'); + } + showOrServeAsset(responseType, fullClaimId, claimName, res); + postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); + }) + .catch(error => { + handleRequestError(originalUrl, ip, error, res); + }); } }); }; diff --git a/testpage.html b/testpage.html new file mode 100644 index 00000000..65dcdb74 --- /dev/null +++ b/testpage.html @@ -0,0 +1,17 @@ + + + + + Test Page + + + + + + + + + + + + \ No newline at end of file diff --git a/views/channel.handlebars b/views/channel.handlebars index d512e675..37eee568 100644 --- a/views/channel.handlebars +++ b/views/channel.handlebars @@ -1,10 +1,10 @@
{{#ifConditional this.totalPages '===' 0}} -

There is no content in {{this.channelName}}:{{this.longChannelId}} yet. Upload some!

+

There is no content in {{this.channelName}}:{{this.longChannelClaimId}} yet. Upload some!

{{/ifConditional}} {{#ifConditional this.totalPages '>=' 1}} -

Below are the contents for {{this.channelName}}:{{this.longChannelId}}

+

Below are the contents for {{this.channelName}}:{{this.longChannelClaimId}}

{{#each this.claims}} {{> gridItem}} @@ -14,21 +14,21 @@ {{#ifConditional this.totalPages '>' 1}}
{{#if this.previousPage}} - Previous + Previous {{else}} Previous {{/if}} | {{#if this.nextPage}} - Next + Next {{else}} Next {{/if}}
{{/ifConditional}} @@ -51,4 +51,4 @@ }); - \ No newline at end of file + diff --git a/views/layouts/channel.handlebars b/views/layouts/channel.handlebars index 7aef87e1..c2832136 100644 --- a/views/layouts/channel.handlebars +++ b/views/layouts/channel.handlebars @@ -1,13 +1,7 @@ - - - - Spee.ch - - - + {{ placeCommonHeaderTags }} @@ -27,4 +21,4 @@ {{> navBar}} {{{ body }}} - \ No newline at end of file + diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index 3d0de0be..782143fe 100644 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -1,13 +1,7 @@ - - - - Spee.ch - - - + {{ placeCommonHeaderTags }} @@ -33,4 +27,4 @@ {{> navBar}} {{{ body }}} - \ No newline at end of file + diff --git a/views/layouts/show.handlebars b/views/layouts/show.handlebars index 47519103..29ecf57c 100644 --- a/views/layouts/show.handlebars +++ b/views/layouts/show.handlebars @@ -1,17 +1,11 @@ - - - - Spee.ch - - - + {{ placeCommonHeaderTags }} - {{#unless fileInfo.nsfw}} - {{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}} - {{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}} + {{#unless claimInfo.nsfw}} + {{{addTwitterCard claimInfo }}} + {{{addOpenGraph claimInfo }}} {{/unless}} @@ -21,9 +15,9 @@ + {{> navBar}} {{{ body }}} - diff --git a/views/layouts/showlite.handlebars b/views/layouts/showlite.handlebars index d446f578..632e1a83 100644 --- a/views/layouts/showlite.handlebars +++ b/views/layouts/showlite.handlebars @@ -1,24 +1,18 @@ - - - - Spee.ch - - - + {{ placeCommonHeaderTags }} - {{#unless fileInfo.nsfw}} - {{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}} - {{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}} - {{/unless}} + {{#unless claimInfo.nsfw}} + {{{addTwitterCard claimInfo }}} + {{{addOpenGraph claimInfo }}} + {{/unless}} {{ googleAnalytics }} + {{{ body }}} - diff --git a/views/partials/asset.handlebars b/views/partials/asset.handlebars index ad16bb9f..00eb83d9 100644 --- a/views/partials/asset.handlebars +++ b/views/partials/asset.handlebars @@ -1,29 +1,37 @@ -{{#ifConditional fileInfo.fileType '===' 'video/mp4'}} - {{#ifConditional fileInfo.fileExt '===' 'gifv'}} - - {{else}} +
+
+ + +
+ +
+ + \ No newline at end of file diff --git a/views/partials/assetInfo.handlebars b/views/partials/assetInfo.handlebars index 8677710d..31d6b02f 100644 --- a/views/partials/assetInfo.handlebars +++ b/views/partials/assetInfo.handlebars @@ -1,28 +1,28 @@ -{{#if fileInfo.channelName}} +{{#if claimInfo.channelName}} {{/if}} -{{#if fileInfo.description}} +{{#if claimInfo.description}}
- {{fileInfo.description}} + {{claimInfo.description}}
{{/if}}