fixed merge conflicts

This commit is contained in:
bill bittner 2017-12-15 10:09:01 -08:00
commit 579c585d00
52 changed files with 1262 additions and 929 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

@ -22,4 +22,13 @@ module.exports = {
files: {
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
},
site: {
name: 'Spee.ch',
host: 'https://spee.ch',
},
publishing: {
defaultTitle : 'Spee.ch',
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
defaultDescription: 'Open-source, decentralized image and video sharing.',
},
};

View file

@ -11,7 +11,7 @@ module.exports = {
// publish the file
return lbryApi.publishClaim(publishParams)
.then(tx => {
logger.info(`Successfully published ${fileName}`, tx);
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
publishResults = tx;
// get the channel information
if (publishParams.channel_name) {
@ -80,6 +80,7 @@ module.exports = {
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
})
.catch(error => {
logger.error('PUBLISH ERROR', error);
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
reject(error);
});

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

@ -1,9 +1,10 @@
const db = require('../models'); // require our models for syncing
// const db = require('../models'); // require our models for syncing
const logger = require('winston');
module.exports = {
populateLocalsDotUser (req, res, next) {
if (req.user) {
logger.debug('populating res.locals.user');
res.locals.user = {
id : req.user.id,
userName : req.user.userName,
@ -14,31 +15,12 @@ module.exports = {
}
next();
},
serializeSpeechUser (user, done) {
done(null, user.id);
serializeSpeechUser (user, done) { // returns user data to be serialized into session
logger.debug('serializing user');
done(null, user);
},
deserializeSpeechUser (id, done) {
let userInfo = {};
db.User.findOne({ where: { id } })
.then(user => {
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
return user.getChannel();
})
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
// return done(null, userInfo);
done(null, userInfo);
return null;
})
.catch(error => {
logger.error(error);
done(error, null);
});
deserializeSpeechUser (user, done) { // deserializes session and populates additional info to req.user
logger.debug('deserializing user');
done(null, user);
},
};

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) {
@ -22,7 +21,7 @@ module.exports = {
} else {
message = error.response;
}
// check for spee.ch thrown errors
// check for thrown errors
} else if (error.message) {
status = 400;
message = error.message;
@ -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

@ -1,9 +1,13 @@
const Handlebars = require('handlebars');
const config = require('../config/speechConfig.js');
const { site, analytics } = 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>${site.title}</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 googleApiKey = analytics.googleId;
const gaCode = `<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
@ -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="${site.title}" />`;
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,6 +1,6 @@
const logger = require('winston');
const fs = require('fs');
const config = require('../config/speechConfig.js');
const { site, wallet } = require('../config/speechConfig.js');
module.exports = {
parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) {
@ -131,12 +131,12 @@ module.exports = {
metadata : {
description,
title,
author : 'spee.ch',
author : site.title,
language: 'en',
license,
nsfw,
},
claim_address: config.wallet.lbryClaimAddress,
claim_address: wallet.lbryClaimAddress,
};
// add thumbnail to channel if video
if (thumbnail !== null) {

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,25 @@
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 file: ${filePath}`);
// 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) {
logger.verbose(`showing claim: ${claimInfo.name}#${claimInfo.claimId}`);
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) {
logger.verbose(`showlite claim: ${claimInfo.name}#${claimInfo.claimId}`);
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(
@ -94,7 +93,6 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
Certificate.associate = db => {
Certificate.belongsTo(db.Channel, {
onDelete : 'cascade',
foreignKey: {
allowNull: true,
},
@ -123,14 +121,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 +136,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 +158,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 +169,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,90 @@
const logger = require('winston');
const { returnShortId } = require('../helpers/sequelizeHelpers.js');
const NO_CLAIM = 'NO_CLAIM';
const { publishing, site } = require('../config/speechConfig.js');
const { defaultTitle, defaultThumbnail, defaultDescription } = publishing;
const { host } = site;
function determineFileExtensionFromContentType (contentType) {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
return 'jpeg';
case 'image/png':
return 'png';
case 'image/gif':
return 'gif';
case 'video/mp4':
return 'mp4';
default:
logger.debug('setting unknown file type as file extension jpeg');
return 'jpeg';
}
};
function determineContentTypeFromFileExtension (fileExtension) {
switch (fileExtension) {
case 'jpeg':
case 'jpg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'mp4':
return 'video/mp4';
default:
logger.debug('setting unknown file type as type image/jpeg');
return 'image/jpeg';
}
};
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['host'] = host;
claim['embedUrl'] = `${host}/${claim.claimId}/${claim.name}`;
claim['showUrl'] = `${host}/${claim.claimId}/${claim.name}`;
claim['source'] = `${host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
claim['directFileUrl'] = `${host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
claim['ogTitle'] = determineOgTitle(claim.title, defaultTitle);
claim['ogDescription'] = determineOgDescription(claim.description, defaultDescription);
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, defaultThumbnail);
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
claim = addOpengraphDataToClaim(claim);
return claim;
};
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
const Claim = sequelize.define(
@ -151,7 +235,6 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
Claim.associate = db => {
Claim.belongsTo(db.File, {
onDelete : 'cascade',
foreignKey: {
allowNull: true,
},
@ -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, defaultThumbnail);
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

@ -27,7 +27,6 @@ module.exports = (sequelize, { STRING, BOOLEAN, TEXT }) => {
Request.associate = db => {
Request.belongsTo(db.File, {
onDelete : 'cascade',
foreignKey: {
allowNull: true,
},

View file

@ -28,9 +28,40 @@ module.exports = (sequelize, { STRING }) => {
bcrypt.compare(password, this.password, callback);
};
User.prototype.changePassword = function (newPassword) {
return new Promise((resolve, reject) => {
// generate a salt string to use for hashing
bcrypt.genSalt((saltError, salt) => {
if (saltError) {
logger.error('salt error', saltError);
reject(saltError);
return;
}
// generate a hashed version of the user's password
bcrypt.hash(newPassword, salt, (hashError, hash) => {
// if there is an error with the hash generation return the error
if (hashError) {
logger.error('hash error', hashError);
reject(hashError);
return;
}
// replace the current password with the new hash
this
.update({password: hash})
.then(() => {
resolve();
})
.catch(error => {
reject(error);
});
});
});
});
};
// pre-save hook method to hash the user's password before the user's info is saved to the db.
User.hook('beforeCreate', (user, options) => {
logger.debug('...beforeCreate hook...');
logger.debug('User.beforeCreate hook...');
return new Promise((resolve, reject) => {
// generate a salt string to use for hashing
bcrypt.genSalt((saltError, salt) => {

View file

@ -1,53 +1,60 @@
const PassportLocalStrategy = require('passport-local').Strategy;
const db = require('../models');
const logger = require('winston');
function returnUserAndChannelInfo (userInstance) {
return new Promise((resolve, reject) => {
let userInfo = {};
userInfo['id'] = userInstance.id;
userInfo['userName'] = userInstance.userName;
userInstance
.getChannel()
.then(({channelName, channelClaimId}) => {
userInfo['channelName'] = channelName;
userInfo['channelClaimId'] = channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
resolve(userInfo);
})
.catch(error => {
reject(error);
});
});
}
module.exports = new PassportLocalStrategy(
{
usernameField : 'username', // username key in the request body
passwordField : 'password', // password key in the request body
session : false,
passReqToCallback: true,
usernameField: 'username',
passwordField: 'password',
},
(req, username, password, done) => {
logger.debug(`verifying loggin attempt ${username} ${password}`);
let userInfo = {};
return db.User
(username, password, done) => {
logger.debug('logging user in');
return db
.User
.findOne({where: {userName: username}})
.then(user => {
if (!user) {
logger.debug('no user found');
// logger.debug('no user found');
return done(null, false, {message: 'Incorrect username or password.'});
}
logger.debug('user found:', user.dataValues);
logger.debug('...comparing password...');
return user.comparePassword(password, (passwordErr, isMatch) => {
user.comparePassword(password, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('passwordErr:', passwordErr);
return done(passwordErr);
return done(null, false, {message: passwordErr});
}
if (!isMatch) {
logger.debug('incorrect password');
// logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password.'});
}
logger.debug('...password was a match...');
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
// get the User's channel info
return user.getChannel()
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
logger.debug('Password was a match, returning User');
return returnUserAndChannelInfo(user)
.then((userInfo) => {
return done(null, userInfo);
})
.catch(error => {
throw error;
return done(error);
});
});
})

View file

@ -5,12 +5,10 @@ const logger = require('winston');
module.exports = new PassportLocalStrategy(
{
usernameField : 'username', // sets the custom name of parameters in the POST body message
passwordField : 'password', // sets the custom name of parameters in the POST body message
session : false, // set to false because we will use token approach to auth
passReqToCallback: true, // we want to be able to read the post body message parameters in the callback
usernameField: 'username',
passwordField: 'password',
},
(req, username, password, done) => {
(username, password, done) => {
logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`);
let userInfo = {};
// server-side validaton of inputs (username, password)

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;
}
@ -476,6 +466,10 @@ table {
display: inline-block;
}
.nav-bar-logo {
cursor: pointer;
}
/* PUBLISH FORM */
.dropzone {
@ -511,27 +505,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');
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

@ -33,15 +33,13 @@ function publishNewChannel (event) {
return sendAuthRequest(userName, password, '/signup') // post the request
})
.then(result => {
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
showChannelCreateDoneDisplay();
// refresh window logged in as the channel
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // set cookies
})
.then(() => {
// if user is on the home page, update the needed elements without reloading
if (window.location.pathname === '/') {
// remove old channel and replace with new one & select it
replaceChannelOptionInPublishChannelSelect();
replaceChannelOptionInNavBarChannelSelect();
replaceChannelOptionInPublishChannelSelect(result.channelName);
replaceChannelOptionInNavBarChannelSelect(result.channelName);
// if user is not on home page, redirect to home page
} else {
window.location = '/';
}
@ -52,7 +50,7 @@ function publishNewChannel (event) {
validationFunctions.showError(channelNameErrorDisplayElement, error.message);
} else {
console.log('signup failure:', error);
showChannelCreationError('Unfortunately, Spee.ch encountered an error while creating your channel. Please let us know in slack!');
showChannelCreationError('Unfortunately, we encountered an error while creating your channel. Please let us know in slack!');
}
})
}
}

View file

@ -1,11 +1,9 @@
function replaceChannelOptionInPublishChannelSelect() {
function replaceChannelOptionInPublishChannelSelect(loggedInChannel) {
// remove the old channel option
const oldChannel = document.getElementById('publish-channel-select-channel-option')
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// get channel details from cookies
const loggedInChannel = getCookie('channel_name');
// create new channel option
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
@ -19,14 +17,12 @@ function replaceChannelOptionInPublishChannelSelect() {
toggleSelectedChannel(loggedInChannel);
}
function replaceChannelOptionInNavBarChannelSelect () {
function replaceChannelOptionInNavBarChannelSelect (loggedInChannel) {
// remove the old channel option
const oldChannel = document.getElementById('nav-bar-channel-select-channel-option');
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// get channel details from cookies
const loggedInChannel = getCookie('channel_name');
// create new channel option & select it
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
@ -49,20 +45,15 @@ function loginToChannel (event) {
event.preventDefault()
validationFunctions.validateNewChannelLogin(userName, password)
.then(() => {
// send request
return sendAuthRequest(userName, password, '/login')
})
.then(result => {
// update session cookie with new channel name and id's
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // replace the current cookies
})
.then(() => {
// update channel selection
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
// if user is on the home page, update the needed elements without reloading
if (window.location.pathname === '/') {
// remove old channel and replace with new one & select it
replaceChannelOptionInPublishChannelSelect();
// remove old channel and replace with new one & select it
replaceChannelOptionInNavBarChannelSelect();
replaceChannelOptionInPublishChannelSelect(result.channelName);
replaceChannelOptionInNavBarChannelSelect(result.channelName);
// if user is not on home page, redirect to home page
} else {
window.location = '/';
}

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;
@ -123,12 +123,14 @@ const publishFileFunctions = {
xhr.open("POST", uri, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
console.log('publish response:', xhr.response)
if (xhr.status == 200) {
console.log('publish complete!');
that.showFilePublishComplete(JSON.parse(xhr.response).message);
console.log('publish complete!');
that.showFilePublishComplete(JSON.parse(xhr.response).message);
} else if (xhr.status == 502){
that.showFilePublishFailure('Spee.ch was not able to get a response from the LBRY network.');
} else {
console.log(xhr.response);
that.showFilePublishFailure(JSON.parse(xhr.response).message);
that.showFilePublishFailure(JSON.parse(xhr.response).message);
}
} else {
console.log('xhr.readyState', xhr.readyState, 'xhr.status', xhr.status);

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

@ -1,39 +1,96 @@
const logger = require('winston');
const multipart = require('connect-multiparty');
const config = require('../config/speechConfig.js');
const multipartMiddleware = multipart({uploadDir: config.files.uploadDirectory});
const { files, site } = require('../config/speechConfig.js');
const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
const db = require('../models');
const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel } = 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 check whether spee.ch has published to a claim
app.get('/api/isClaimAvailable/:name', ({ params }, res) => {
// send response
// 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 this site published to a claim
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 by this site`);
res.status(200).json(false);
}
})
@ -41,34 +98,29 @@ module.exports = (app) => {
res.status(500).json(error);
});
});
// route to check whether spee.ch has published to a channel
app.get('/api/isChannelAvailable/:name', ({ params }, res) => {
// route to check whether site has published to a channel
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`);
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
@ -96,7 +148,7 @@ module.exports = (app) => {
})
.then(result => {
if (!result) {
throw new Error('That name is already in use by spee.ch.');
throw new Error('That name is already claimed by another user.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
@ -111,19 +163,17 @@ module.exports = (app) => {
success: true,
message: {
name,
url : `spee.ch/${result.claim_id}/${name}`,
url : `${site.host}/${result.claim_id}/${name}`,
lbryTx: result,
},
});
})
.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);
@ -134,8 +184,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);
@ -143,7 +192,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

@ -14,7 +14,7 @@ module.exports = (app) => {
});
// route for log in
app.post('/login', passport.authenticate('local-login'), (req, res) => {
logger.debug('req.user:', req.user);
// logger.debug('req.user:', req.user); // req.user contains the authenticated user's info
logger.debug('successful login');
res.status(200).json({
success : true,

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

@ -1,5 +1,6 @@
const errorHandlers = require('../helpers/errorHandlers.js');
const { getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
const { site } = require('../config/speechConfig.js');
module.exports = (app) => {
// route to log out
@ -15,7 +16,7 @@ module.exports = (app) => {
res.status(200).render('login');
}
});
// route to show 'about' page for spee.ch
// route to show 'about' page
app.get('/about', (req, res) => {
// get and render the content
res.status(200).render('about');
@ -30,32 +31,31 @@ 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)
app.get('/embed/:claimId/:name', ({ params }, res) => {
const claimId = params.claimId;
const name = params.name;
const host = site.host;
// get and render the content
res.status(200).render('embed', { layout: 'embed', claimId, name });
res.status(200).render('embed', { layout: 'embed', host, claimId, name });
});
// route to display all free public claims at a given name
app.get('/:name/all', (req, res) => {

View file

@ -1,258 +1,216 @@
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 ({accept}) {
return accept && accept.match(/text\/html/);
}
function requestIsFromBrowser (headers) {
return headers['user-agent'] && headers['user-agent'].match(/Mozilla/);
};
function clientWantsAsset ({accept, range}) {
const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/);
const videoIsWanted = accept && range;
return imageIsWanted || videoIsWanted;
}
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 (clientWantsAsset(headers) && requestIsFromBrowser(headers)) { // this is in case someone embeds a show url
logger.debug('Show request came from browser and wants an image/video; changing response to serve.');
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 '/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);
});
}
});
};

View file

@ -33,10 +33,16 @@ app.use(bodyParser.json()); // 'body parser' for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing application/x-www-form-urlencoded
app.use((req, res, next) => { // custom logging middleware to log all incoming http requests
logger.verbose(`Request on ${req.originalUrl} from ${req.ip}`);
logger.debug('req.body:', req.body);
next();
});
// configure passport
passport.serializeUser(serializeSpeechUser);
passport.deserializeUser(deserializeSpeechUser);
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
passport.use('local-login', localLoginStrategy);
// initialize passport
app.use(cookieSession({
name : 'session',
@ -45,12 +51,6 @@ app.use(cookieSession({
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser(serializeSpeechUser); // takes the user id from the db and serializes it
passport.deserializeUser(deserializeSpeechUser); // this deserializes id then populates req.user with info
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
passport.use('local-login', localLoginStrategy);
// configure handlebars & register it with express app
const hbs = expressHandlebars.create({

View file

@ -0,0 +1,47 @@
// load dependencies
const logger = require('winston');
const db = require('../models/index'); // require our models for syncing
// configure logging
const config = require('../config/speechConfig.js');
const logLevel = config.logging.logLevel;
require('../config/loggerConfig.js')(logger, logLevel);
const userName = process.argv[2];
logger.debug('user name:', userName);
const oldPassword = process.argv[3];
logger.debug('old password:', oldPassword);
const newPassword = process.argv[4];
logger.debug('new password:', newPassword);
db.sequelize.sync() // sync sequelize
.then(() => {
logger.info('finding user profile');
return db.User.findOne({
where: {
userName: userName,
},
});
})
.then(user => {
if (!user) {
throw new Error('no user found');
}
return new Promise((resolve, reject) => {
user.comparePassword(oldPassword, (passwordErr, isMatch) => {
if (passwordErr) {
return reject(passwordErr);
}
if (!isMatch) {
return reject('Incorrect old password.');
}
logger.debug('Password was a match, updating password');
return resolve(user.changePassword(newPassword));
});
});
})
.then(() => {
logger.debug('Password successfully updated');
})
.catch((error) => {
logger.error(error);
});

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://dev1.spee.ch/zackmath"/>
<!--<img src="https://dev1.spee.ch/8/zackmath"/>-->
<!--<img src="https://dev1.spee.ch/zackmath.ext"/>-->
<!--<img src="https://dev1.spee.ch/8/zackmath.ext"/>-->
<video width="50%" controls poster="https://dev1.spee.ch/assets/img/video_thumb_default.png" src="https://dev1.spee.ch/LBRY-Hype"></video>
<!--<video width="50%" controls poster="https://dev1.spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/a/LBRY-Hype"></video>-->
<!--<video width="50%" controls poster="https://dev1.spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/LBRY-Hype.test"></video>-->
<!--<video width="50%" controls poster="https://dev1.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}}
@ -51,4 +51,4 @@
});
</script>
</script>

View file

@ -1 +1 @@
<video width="100%" controls src="https://spee.ch/{{claimId}}/{{name}}.mp4" type="video/mp4"></video>
<video width="100%" controls src="{{host}}/{{claimId}}/{{name}}.mp4" type="video/mp4"></video>

View file

@ -1,21 +1,15 @@
<!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">
<meta property="og:site_name" content="Spee.ch">
<meta property="og:type" content="website">
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg">
<meta property="og:url" content="http://spee.ch/{{this.channelName}}:{{this.longChannelId}}">
<meta property="og:description" content="View images and videos from {{this.channelName}}">
<meta property="og:title" content="{{this.channelName}} on Spee.ch" />
<meta property="og:site_name" content="Spee.ch" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg" />
<meta property="og:url" content="http://spee.ch/{{this.channelName}}:{{this.longChannelId}}" />
<meta property="og:description" content="View images and videos from {{this.channelName}}" />
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
<!-- google analytics -->
@ -27,4 +21,4 @@
{{> navBar}}
{{{ body }}}
</body>
</html>
</html>

View file

@ -1,21 +1,15 @@
<!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">
<meta property="og:site_name" content="Spee.ch">
<meta property="og:type" content="website">
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg">
<meta property="og:url" content="http://spee.ch/">
<meta property="og:description" content="Open-source, decentralized image and video sharing.">
<meta property="og:title" content="Spee.ch" />
<meta property="og:site_name" content="Spee.ch" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg" />
<meta property="og:url" content="http://spee.ch/" />
<meta property="og:description" content="Open-source, decentralized image and video sharing." />
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
<!-- google analytics -->
@ -33,4 +27,4 @@
{{> navBar}}
{{{ body }}}
</body>
</html>
</html>

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="{{claimInfo.host}}/{{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="{{claimInfo.host}}/{{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="{{claimInfo.host}}/{{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={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">twitter</a>
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">facebook</a>
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">tumblr</a>
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url={{claimInfo.host}}/{{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>
</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>