Development #287

Merged
bones7242 merged 58 commits from development into master 2017-12-11 20:53:02 +01:00
39 changed files with 1073 additions and 820 deletions

View file

@ -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)

View file

@ -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.');
}
};

View file

@ -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;
});
},
};

View file

@ -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);
});
});

View file

@ -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;
},
};

View file

@ -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)

View file

@ -2,6 +2,10 @@ const Handlebars = require('handlebars');
const config = require('../config/speechConfig.js');
module.exports = {
placeCommonHeaderTags () {
const headerBoilerplate = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Spee.ch</title><link rel="stylesheet" href="/assets/css/reset.css" type="text/css"><link rel="stylesheet" href="/assets/css/general.css" type="text/css"><link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">`;
return new Handlebars.SafeString(headerBoilerplate);
},
googleAnalytics () {
const googleApiKey = config.analytics.googleId;
const gaCode = `<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
@ -12,41 +16,35 @@ module.exports = {
ga('send', 'pageview');</script>`;
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 = `<meta property="og:title" content="${title}" >`;
const ogUrl = `<meta property="og:url" content="${showUrl}" >`;
const ogSiteName = `<meta property="og:site_name" content="Spee.ch" >`;
const ogDescription = `<meta property="og:description" content="${description}" >`;
const ogImageWidth = '<meta property="og:image:width" content="600" >';
const ogImageHeight = '<meta property="og:image:height" content="315" >';
const basicTags = `${ogTitle} ${ogUrl} ${ogSiteName} ${ogDescription} ${ogImageWidth} ${ogImageHeight}`;
let ogImage = `<meta property="og:image" content="${source}" >`;
let ogImageType = `<meta property="og:image:type" content="${mimeType}" >`;
let ogType = `<meta property="og:type" content="article" >`;
if (mimeType === 'video/mp4') {
const ogVideo = `<meta property="og:video" content="${source}" >`;
const ogVideoSecureUrl = `<meta property="og:video:secure_url" content="${source}" >`;
const ogVideoType = `<meta property="og:video:type" content="${mimeType}" >`;
ogImage = `<meta property="og:image" content="${thumbnail}" >`;
ogImageType = `<meta property="og:image:type" content="image/png" >`;
ogType = `<meta property="og:type" content="video" >`;
return new Handlebars.SafeString(`${basicTags} ${ogImage} ${ogImageType} ${ogType} ${ogVideo} ${ogVideoSecureUrl} ${ogVideoType}`);
addOpenGraph ({ ogTitle, contentType, ogDescription, thumbnail, showUrl, source, ogThumbnailContentType }) {
const ogTitleTag = `<meta property="og:title" content="${ogTitle}" >`;
const ogUrlTag = `<meta property="og:url" content="${showUrl}" >`;
const ogSiteNameTag = `<meta property="og:site_name" content="Spee.ch" >`;
const ogDescriptionTag = `<meta property="og:description" content="${ogDescription}" >`;
const ogImageWidthTag = '<meta property="og:image:width" content="600" >';
const ogImageHeightTag = '<meta property="og:image:height" content="315" >';
const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`;
let ogImageTag = `<meta property="og:image" content="${source}" >`;
let ogImageTypeTag = `<meta property="og:image:type" content="${contentType}" >`;
let ogTypeTag = `<meta property="og:type" content="article" >`;
if (contentType === 'video/mp4') {
const ogVideoTag = `<meta property="og:video" content="${source}" >`;
const ogVideoSecureUrlTag = `<meta property="og:video:secure_url" content="${source}" >`;
const ogVideoTypeTag = `<meta property="og:video:type" content="${contentType}" >`;
ogImageTag = `<meta property="og:image" content="${thumbnail}" >`;
ogImageTypeTag = `<meta property="og:image:type" content="${ogThumbnailContentType}" >`;
ogTypeTag = `<meta property="og:type" content="video" >`;
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`);
} else {
if (mimeType === 'image/gif') {
ogType = `<meta property="og:type" content="video.other" >`;
if (contentType === 'image/gif') {
ogTypeTag = `<meta property="og:type" content="video.other" >`;
};
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 = `<meta name="twitter:site" content="@spee_ch" >`;
if (mimeType === 'video/mp4') {
if (contentType === 'video/mp4') {
const twitterName = '<meta name="twitter:card" content="player" >';
const twitterPlayer = `<meta name="twitter:player" content="${embedUrl}" >`;
const twitterPlayerWidth = '<meta name="twitter:player:width" content="600" >';

View file

@ -10,7 +10,6 @@ function handleLbrynetResponse ({ data }, resolve, reject) {
reject(data.result.error);
return;
};
// logger.debug('data.result', data.result);
resolve(data.result);
return;
}

90
helpers/lbryUri.js Normal file
View file

@ -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,
};
},
};

View file

@ -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;

View file

@ -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 });
},
};

View file

@ -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
}

View file

@ -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<ch';
const DEFAULT_DESCRIPTION = 'Decentralized video and content hosting.';
function determineFileExtensionFromContentType (contentType) {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
return 'jpg';
case 'image/png':
return 'png';
case 'image/gif':
return 'gif';
case 'video/mp4':
return 'mp4';
default:
logger.info('setting unknown file type as file extension jpg');
return 'jpg';
}
};
function determineContentTypeFromFileExtension (fileExtension) {
switch (fileExtension) {
case 'jpeg':
case 'jpg':
return 'image/jpg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'mp4':
return 'video/mp4';
default:
logger.info('setting unknown file type as type image/jpg');
return 'image/jpg';
}
};
function ifEmptyReturnOther (value, replacement) {
if (value === '') {
return replacement;
}
return value;
}
function determineThumbnail (storedThumbnail, defaultThumbnail) {
return ifEmptyReturnOther(storedThumbnail, defaultThumbnail);
};
function determineOgTitle (storedTitle, defaultTitle) {
return ifEmptyReturnOther(storedTitle, defaultTitle);
};
function determineOgDescription (storedDescription, defaultDescription) {
return ifEmptyReturnOther(storedDescription, defaultDescription);
};
function determineOgThumbnailContentType (thumbnail) {
if (thumbnail) {
if (thumbnail.lastIndexOf('.') !== -1) {
return determineContentTypeFromFileExtension(thumbnail.substring(thumbnail.lastIndexOf('.')));
}
}
return '';
}
function addOpengraphDataToClaim (claim) {
claim['embedUrl'] = `https://spee.ch/embed/${claim.claimId}/${claim.name}`;
claim['showUrl'] = `https://spee.ch/${claim.claimId}/${claim.name}`;
claim['source'] = `https://spee.ch/${claim.claimId}/${claim.name}.${claim.fileExt}`;
claim['directFileUrl'] = `https://spee.ch/${claim.claimId}/${claim.name}.${claim.fileExt}`;
claim['ogTitle'] = determineOgTitle(claim.title, DEFAULT_TITLE);
claim['ogDescription'] = determineOgDescription(claim.description, DEFAULT_DESCRIPTION);
claim['ogThumbnailContentType'] = determineOgThumbnailContentType(claim.thumbnail);
return claim;
};
function prepareClaimData (claim) {
// logger.debug('preparing claim data based on resolved data:', claim);
claim['thumbnail'] = determineThumbnail(claim.thumbnail, DEFAULT_THUMBNAIL);
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
claim = addOpengraphDataToClaim(claim);
return claim;
};
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
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 => {

View file

@ -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 });
};

View file

@ -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 */

View file

@ -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) {

View file

@ -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();
})
};
};

View file

@ -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);
};
};

View file

@ -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;

View file

@ -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);
}
};
}

View file

@ -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();

View file

@ -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);
});
});
};

View file

@ -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');
});

View file

@ -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)

View file

@ -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);
});
}
});
};

17
testpage.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Page</title>
</head>
<body>
<img src="https://staging.spee.ch/zackmath"/>
<img src="https://staging.spee.ch/8/zackmath"/>
<img src="https://staging.spee.ch/zackmath.ext"/>
<img src="https://staging.spee.ch/8/zackmath.ext"/>
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/LBRY-Hype"></video>
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/a/LBRY-Hype"></video>
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/LBRY-Hype.test"></video>
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/a/LBRY-Hype.test"></video>
</body>
</html>

View file

@ -1,10 +1,10 @@
<div class="row row--padded">
<div class="row">
{{#ifConditional this.totalPages '===' 0}}
<p>There is no content in {{this.channelName}}:{{this.longChannelId}} yet. Upload some!</p>
<p>There is no content in {{this.channelName}}:{{this.longChannelClaimId}} yet. Upload some!</p>
{{/ifConditional}}
{{#ifConditional this.totalPages '>=' 1}}
<p>Below are the contents for {{this.channelName}}:{{this.longChannelId}}</p>
<p>Below are the contents for {{this.channelName}}:{{this.longChannelClaimId}}</p>
<div class="grid">
{{#each this.claims}}
{{> gridItem}}
@ -14,21 +14,21 @@
{{#ifConditional this.totalPages '>' 1}}
<div class="row">
<div class="column column--3 align-content--left">
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p=1">First [1]</a>
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p=1">First [1]</a>
</div><div class="column column--4 align-content-center">
{{#if this.previousPage}}
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.previousPage}}">Previous</a>
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.previousPage}}">Previous</a>
{{else}}
<a disabled>Previous</a>
{{/if}}
|
{{#if this.nextPage}}
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.nextPage}}">Next</a>
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.nextPage}}">Next</a>
{{else}}
<a disabled>Next</a>
{{/if}}
</div><div class="column column--3 align-content-right">
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
</div>
</div>
{{/ifConditional}}

View file

@ -1,13 +1,7 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
{{ placeCommonHeaderTags }}
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@spee_ch" />
<meta property="og:title" content="{{this.channelName}} on Spee.ch">

View file

@ -1,13 +1,7 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
{{ placeCommonHeaderTags }}
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@spee_ch" />
<meta property="og:title" content="Spee.ch">

View file

@ -1,17 +1,11 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
{{ placeCommonHeaderTags }}
<meta property="fb:app_id" content="1371961932852223">
{{#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}}
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
@ -21,9 +15,9 @@
<body id="show-body">
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
<script src="/assets/js/assetConstructor.js"></script>
{{> navBar}}
{{{ body }}}
<script src="/assets/js/showFunctions.js"></script>
</body>
</html>

View file

@ -1,24 +1,18 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
{{ placeCommonHeaderTags }}
<meta property="fb:app_id" content="1371961932852223">
{{#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}}
<!-- google analytics -->
{{ googleAnalytics }}
</head>
<body id="showlite-body">
<script src="/assets/js/assetConstructor.js"></script>
{{{ body }}}
<script src="/assets/js/showFunctions.js"></script>
</body>
</html>

View file

@ -1,29 +1,37 @@
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
{{#ifConditional fileInfo.fileExt '===' 'gifv'}}
<video class="gifv-show" autoplay loop muted>
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{else}}
<div id="asset-display-component">
<div id="asset-status">
<div id="searching-message" hidden="true">
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
{{> progressBar}}
<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
</div>
<div id="failure-message" hidden="true">
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY discord</a>.</p>
<i><p id="error-message"></p></i>
</div>
</div>
<div id="asset-holder" hidden="true">
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
{{> video}}
{{else}}
{{> image}}
{{/ifConditional}}
<div>
<a id="asset-boilerpate" class="link--primary fine-print" href="/{{claimInfo.claimId}}/{{claimInfo.name}}">hosted via Spee&lt;h</a>
</div>
<video id="video-player" class="video-show video" controls poster="{{fileInfo.thumbnail}}">
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', resizeVideoPlayer)
window.addEventListener("resize", resizeVideoPlayer);
function resizeVideoPlayer() {
const div = document.getElementById('video-player');
const width = div.offsetWidth;
div.height = (width * 9 / 16);
}
</script>
{{/ifConditional}}
{{else}}
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
<img class="image-show" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" />
</a>
{{/ifConditional}}
</div>
</div>
<script type="text/javascript">
const asset = new Asset();
asset.data['src'] = '/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}';
asset.data['claimName'] = '{{claimInfo.name}}';
asset.data['claimId'] = '{{claimInfo.claimId}}';
asset.data['fileExt'] = '{{claimInfo.fileExt}}';
asset.data['contentType'] = '{{claimInfo.contentType}}';
console.log('asset data:', asset.data);
asset.checkFileAndRenderAsset();
</script>

View file

@ -1,28 +1,28 @@
{{#if fileInfo.channelName}}
{{#if claimInfo.channelName}}
<div class="row row--padded row--wide row--no-top">
<div class="column column--2 column--med-10">
<span class="text">Channel:</span>
</div><div class="column column--8 column--med-10">
<span class="text"><a href="/{{fileInfo.channelName}}:{{fileInfo.certificateId}}">{{fileInfo.channelName}}</a></span>
<span class="text"><a href="/{{claimInfo.channelName}}:{{claimInfo.certificateId}}">{{claimInfo.channelName}}</a></span>
</div>
</div>
{{/if}}
{{#if fileInfo.description}}
{{#if claimInfo.description}}
<div class="row row--padded row--wide row--no-top">
<span class="text">{{fileInfo.description}}</span>
<span class="text">{{claimInfo.description}}</span>
</div>
{{/if}}
<div class="row row--wide">
<div id="show-short-link">
<div class="column column--2 column--med-10">
<a class="link--primary" href="/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"><span class="text">Link:</span></a>
<a class="link--primary" href="/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"><span class="text">Link:</span></a>
</div><div class="column column--8 column--med-10">
<div class="row row--short row--wide">
<div class="column column--7">
<div class="input-error" id="input-error-copy-short-link" hidden="true"></div>
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" onclick="select()"/>
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="https://spee.ch/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}" onclick="select()"/>
</div><div class="column column--1"></div><div class="column column--2">
<button class="button--primary" data-elementtocopy="short-link" onclick="copyToClipboard(event)">copy</button>
</div>
@ -36,10 +36,10 @@
<div class="row row--short row--wide">
<div class="column column--7">
<div class="input-error" id="input-error-copy-embed-text" hidden="true"></div>
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;video width="100%" controls poster="{{fileInfo.thumbnail}}" src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>&lt;/video>'/>
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;video width="100%" controls poster="{{claimInfo.thumbnail}}" src="https://spee.ch/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/>&lt;/video>'/>
{{else}}
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;img src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>'/>
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;img src="https://spee.ch/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/>'/>
{{/ifConditional}}
</div><div class="column column--1"></div><div class="column column--2">
<button class="button--primary" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
@ -55,10 +55,10 @@
<span class="text">Share:</span>
</div><div class="column column--7 column--med-10">
<div class="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">twitter</a>
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">facebook</a>
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">tumblr</a>
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}&title={{fileInfo.name}}">reddit</a>
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text=https://spee.ch/{{shortId}}/{{claimInfo.name}}">twitter</a>
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://spee.ch/{{shortId}}/{{claimInfo.name}}">facebook</a>
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl=https://spee.ch/{{shortId}}/{{claimInfo.name}}">tumblr</a>
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url=https://spee.ch/{{shortId}}/{{claimInfo.name}}&title={{claimInfo.name}}">reddit</a>
</div>
</div>
</div>
@ -73,29 +73,29 @@
<div class="column column--2 column--med-10">
<span class="text">Name:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.name}}
{{claimInfo.name}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">Claim Id:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.claimId}}
{{claimInfo.claimId}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Name:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.fileName}}
{{claimInfo.fileName}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Type:</span>
</div><div class="column column--8 column--med-10">
{{#if fileInfo.fileType}}
{{fileInfo.fileType}}
{{#if claimInfo.contentType}}
{{claimInfo.contentType}}
{{else}}
unknown
{{/if}}

View file

@ -2,11 +2,10 @@
{{#ifConditional this.contentType '===' 'video/mp4'}}
<img class="grid-item-image" src="{{this.thumbnail}}"/>
{{else}}
<img class="grid-item-image" src="{{this.directUrlLong}}" />
<img class="grid-item-image" src="{{this.claimId}}/{{this.name}}.{{this.fileExt}}" />
{{/ifConditional}}
<div class="hidden" onclick="window.location='{{this.showUrlLong}}'">
<div class="hidden" onclick="window.location='{{this.claimId}}/{{this.name}}'">
<p class="grid-item-details-text">{{this.name}}</p>
</div>
</div>

View file

@ -0,0 +1 @@
<img id="image-asset" class="asset"/>

View file

@ -0,0 +1,8 @@
<p id="bar-holder"></p>
<script src="/assets/js/progressBarConstructor.js"></script>
<script type="text/javascript">
const progressBar = new ProgressBar();
progressBar.createProgressBar(10);
progressBar.startProgressBar();
</script>

View file

@ -0,0 +1,10 @@
<video id="video-asset" class="asset" controls poster="{{claimInfo.thumbnail}}">
<source>
<!--fallback-->
Your browser does not support the <code>video</code> element.
</video>
<script type="text/javascript">
showFunctions.addPlayPauseToVideo();
</script>

View file

@ -1,7 +1,7 @@
<div class="row row--tall row--padded">
<div class="column column--10">
<!-- title -->
<span class="text--large">{{fileInfo.title}}</span>
<span class="text--large">{{claimInfo.title}}</span>
</div>
<div class="column column--5 column--sml-10 align-content-top">
<!-- asset -->

View file

@ -1,21 +1,3 @@
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
{{#ifConditional fileInfo.fileExt '===' '.gifv'}}
<video class="showlite-asset" autoplay loop muted>
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{else}}
<video class="showlite-asset" controls id="video-player">
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{/ifConditional}}
<br/>
<a class="link--primary fine-print" href="/{{fileInfo.claimId}}/{{fileInfo.name}}">hosted via spee&lt;h</a>
{{else}}
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}">
<img class="showlite-asset" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" alt="{{fileInfo.fileName}}"/>
</a>
{{/ifConditional}}
<div class="row row--tall flex-container--column flex-container--center-center">
{{> asset }}
</div>