Merge pull request #595 from lbryio/update-claims

allow claims to be updated and abandoned
This commit is contained in:
Travis Eden 2018-11-09 10:50:34 -05:00 committed by GitHub
commit dde6956283
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1080 additions and 280 deletions

View file

@ -14,6 +14,19 @@ export function clearFile () {
};
}
export function setUpdateTrue () {
return {
type: actions.SET_UPDATE_TRUE,
};
}
export function setHasChanged (status) {
return {
type: actions.SET_HAS_CHANGED,
data: status,
};
}
export function updateMetadata (name, value) {
return {
type: actions.METADATA_UPDATE,
@ -31,6 +44,13 @@ export function updateClaim (value) {
};
};
export function abandonClaim (data) {
return {
type: actions.ABANDON_CLAIM,
data,
};
};
export function setPublishInChannel (channel) {
return {
type: actions.SET_PUBLISH_IN_CHANNEL,

View file

@ -105,6 +105,13 @@ export function updateAssetViewsInList (id, claimId, claimViews) {
};
}
export function removeAsset (data) {
return {
type: actions.ASSET_REMOVE,
data,
};
}
// channel actions
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
@ -129,7 +136,7 @@ export function onUpdateChannelClaims (channelKey, name, longId, page) {
export function updateChannelClaims (channelListId, claimsData) {
return {
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS,
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED,
data: {channelListId, claimsData},
};
}

View file

@ -42,3 +42,15 @@ export function getClaimViews (claimId) {
const url = `/api/claim/views/${claimId}`;
return Request(url);
}
export function doAbandonClaim (claimId) {
const params = {
method : 'POST',
body : JSON.stringify({claimId}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
};
return Request('/api/claim/abandon', params);
}

View file

@ -10,6 +10,7 @@ import ContentPageWrapper from '@pages/ContentPageWrapper';
import FourOhFourPage from '@pages/FourOhFourPage';
import MultisitePage from '@pages/MultisitePage';
import PopularPage from '@pages/PopularPage';
import EditPage from '@pages/EditPage';
const App = () => {
return (
@ -21,6 +22,7 @@ const App = () => {
<Route exact path='/login' component={LoginPage} />
<Route exact path='/multisite' component={MultisitePage} />
<Route exact path='/popular' component={PopularPage} />
<Route exact path='/edit/:identifier/:claim' component={EditPage} />
<Route exact path='/:identifier/:claim' component={ContentPageWrapper} />
<Route exact path='/:claim' component={ContentPageWrapper} />
<Route component={FourOhFourPage} />

View file

@ -1,8 +1,8 @@
import {buffers, END, eventChannel} from 'redux-saga';
export const makePublishRequestChannel = (fd) => {
export const makePublishRequestChannel = (fd, isUpdate) => {
return eventChannel(emitter => {
const uri = '/api/claim/publish';
const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`;
const xhr = new XMLHttpRequest();
// add event listeners
const onLoadStart = () => {

View file

@ -2,11 +2,14 @@ import React from 'react';
import FormFeedbackDisplay from '@components/FormFeedbackDisplay';
import Row from '@components/Row';
const DropzoneInstructionsDisplay = ({fileError}) => {
const DropzoneInstructionsDisplay = ({fileError, message}) => {
if (!message) {
message = 'Drag & drop image or video here to publish';
}
return (
<div className={'dropzone-instructions-display'}>
<Row>
<p className={'text--large'}>Drag & drop image or video here to publish</p>
<p className={'text--large'}>{message}</p>
</Row>
<Row>
<p className={'text--small'}>OR</p>

View file

@ -10,7 +10,12 @@ class PublishPreview extends React.Component {
};
}
componentDidMount () {
this.setPreviewImageSource(this.props.file);
const { isUpdate, sourceUrl, file } = this.props;
if (isUpdate && sourceUrl) {
this.setState({ imgSource: sourceUrl });
} else {
this.setPreviewImageSource(file);
}
}
componentWillReceiveProps (newProps) {
if (newProps.file !== this.props.file) {
@ -54,8 +59,10 @@ class PublishPreview extends React.Component {
PublishPreview.propTypes = {
dimPreview: PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
file : PropTypes.object,
thumbnail : PropTypes.object,
isUpdate : PropTypes.bool,
sourceUrl : PropTypes.string,
};
export default PublishPreview;

View file

@ -16,7 +16,7 @@ const PublishLicenseInput = ({ handleSelect }) => {
className='select select--primary'
onChange={handleSelect}
>
<option value=' '>Unspecified</option>
<option value=''>Unspecified</option>
<option value='Public Domain'>Public Domain</option>
<option value='Creative Commons'>Creative Commons</option>
</select>

View file

@ -7,9 +7,11 @@ import Row from '@components/Row';
class PublishPreview extends React.Component {
render () {
const { isUpdate, uri } = this.props;
return (
<div>
<Row>
{isUpdate && uri && (<p className='text--extra-small'>{`Editing ${uri}`}</p>)}
<PublishTitleInput />
</Row>
<HorizontalSplit

View file

@ -0,0 +1 @@
export const SAVE = 'Everything not saved will be lost. Are you sure you want to leave this page?';

View file

@ -10,3 +10,6 @@ export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
export const THUMBNAIL_NEW = 'THUMBNAIL_NEW';
export const PUBLISH_START = 'PUBLISH_START';
export const CLAIM_AVAILABILITY = 'CLAIM_AVAILABILITY';
export const SET_UPDATE_TRUE = 'SET_UPDATE_TRUE';
export const ABANDON_CLAIM = 'ABANDON_CLAIM';
export const SET_HAS_CHANGED = 'SET_HAS_CHANGED';

View file

@ -1,5 +1,6 @@
export const LOAD_START = 'LOAD_START';
export const LOADING = 'LOADING';
export const PUBLISHING = 'PUBLISHING';
export const SUCCESS = 'SUCCESS';
export const SUCCEEDED = 'SUCCEEDED';
export const FAILED = 'FAILED';
export const ABANDONING = 'ABANDONING';

View file

@ -11,12 +11,14 @@ export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
// asset actions
export const ASSET_ADD = 'ASSET_ADD';
export const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';
export const ASSET_UPDATE_CLAIMDATA = 'ASSET_UPDATE_CLAIMDATA';
export const ASSET_REMOVE = 'ASSET_REMOVE';
// channel actions
export const CHANNEL_ADD = 'CHANNEL_ADD';
export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';
export const CHANNEL_CLAIMS_UPDATE_SUCCESS = 'CHANNEL_CLAIMS_UPDATE_SUCCESS';
export const CHANNEL_CLAIMS_UPDATE_SUCCEEDED = 'CHANNEL_CLAIMS_UPDATE_SUCCEEDED';
// asset/file display actions
export const FILE_REQUESTED = 'FILE_REQUESTED';

View file

@ -3,7 +3,8 @@ import View from './view';
import { fileRequested } from '../../actions/show';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const mapStateToProps = (props) => {
const {show} = props;
// select error and status
const error = show.displayAsset.error;
const status = show.displayAsset.status;

View file

@ -2,6 +2,42 @@ import React from 'react';
import Row from '@components/Row';
import ProgressBar from '@components/ProgressBar';
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
import createCanonicalLink from '../../../../utils/createCanonicalLink';
class AvailableContent extends React.Component {
render () {
const {contentType, sourceUrl, name, thumbnail} = this.props;
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return (
<img
className='asset-image'
src={sourceUrl}
alt={name}
/>
);
case 'video/mp4':
return (
<video
className='asset-video'
controls poster={thumbnail}
>
<source
src={sourceUrl}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported content type</p>
);
}
}
}
class AssetDisplay extends React.Component {
componentDidMount () {
@ -9,8 +45,15 @@ class AssetDisplay extends React.Component {
this.props.onFileRequest(name, claimId);
}
render () {
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
const sourceUrl = `/${claimId}/${name}.${fileExt}`;
const { status, error, asset } = this.props;
const { name, claimData: { claimId, contentType, thumbnail, outpoint } } = asset;
// the outpoint is added to force the browser to re-download the asset after an update
// issue: https://github.com/lbryio/spee.ch/issues/607
let fileExt;
if (typeof contentType === 'string') {
fileExt = contentType.split('/')[1] || 'jpg';
}
const sourceUrl = `${createCanonicalLink({ asset: asset.claimData })}.${fileExt}?${outpoint}`;
return (
<div className={'asset-display'}>
{(status === LOCAL_CHECK) &&
@ -36,37 +79,12 @@ class AssetDisplay extends React.Component {
</div>
}
{(status === AVAILABLE) &&
(() => {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return (
<img
className='asset-image'
src={sourceUrl}
alt={name}
/>
);
case 'video/mp4':
return (
<video
className='asset-video'
controls poster={thumbnail}
>
<source
src={sourceUrl}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported content type</p>
);
}
})()
<AvailableContent
contentType={contentType}
sourceUrl={sourceUrl}
name={name}
thumbnail={thumbnail}
/>
}
</div>
);

View file

@ -2,12 +2,20 @@ import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const mapStateToProps = (props) => {
const {show} = props;
// select asset
const asset = selectAsset(show);
const editable = Boolean(
asset &&
asset.claimData &&
asset.claimData.channelName &&
props.channel.loggedInChannel.name === asset.claimData.channelName
);
// return props
return {
asset,
editable,
};
};

View file

@ -13,10 +13,11 @@ import createCanonicalLink from '../../../../utils/createCanonicalLink';
class AssetInfo extends React.Component {
render () {
const { asset } = this.props;
const { claimViews, claimData: { channelName, channelShortId, description, name, fileExt, contentType, thumbnail, host } } = asset;
const { editable, asset } = this.props;
const { claimViews, claimData } = asset;
const { channelName, claimId, channelShortId, description, name, fileExt, contentType, host } = claimData;
const canonicalUrl = createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});
const canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId: asset.shortId }});
const assetCanonicalUrl = `${host}${canonicalUrl}`;
let channelCanonicalUrl;
@ -29,6 +30,15 @@ class AssetInfo extends React.Component {
}
return (
<div>
{editable && (
<Row>
<RowLabeled
label={<Label value={'Edit:'} />}
content={<Link to={`/edit${canonicalUrl}`}>{name}</Link>}
/>
</Row>
)}
{channelName && (
<Row>
<RowLabeled

View file

@ -2,8 +2,8 @@ import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const { claimData: { title } } = selectAsset(show);
const mapStateToProps = (props) => {
const { claimData: { title } } = selectAsset(props.show);
return {
title,
};

View file

@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Row from '@components/Row';
const AssetTitle = ({ title }) => {

View file

@ -36,7 +36,7 @@ class ChannelClaimsDisplay extends React.Component {
<AssetPreview
defaultThumbnail={defaultThumbnail}
claimData={claim}
key={`${claim.name}-${claim.id}`}
key={claim.claimId}
/>
))}
</div>

View file

@ -1,13 +1,29 @@
import { connect } from 'react-redux';
import { selectFile, updateError, clearFile } from '../../actions/publish';
import { selectAsset } from '../../selectors/show';
import View from './view';
import siteConfig from '@config/siteConfig.json';
import createCanonicalLink from '../../../../utils/createCanonicalLink';
const mapStateToProps = ({ publish }) => {
return {
file : publish.file,
thumbnail: publish.thumbnail,
fileError: publish.error.file,
};
const { assetDefaults: { thumbnail: defaultThumbnail } } = siteConfig;
const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate } }) => {
const obj = { file, thumbnail, fileError, isUpdate };
let asset, name, claimId, fileExt, outpoint, sourceUrl;
if (isUpdate) {
asset = selectAsset(show);
const { claimData } = asset;
if (asset) {
obj.fileExt = claimData.contentType.split('/')[1];
if (obj.fileExt === 'mp4') {
obj.sourceUrl = claimData.thumbnail ? claimData.thumbnail : defaultThumbnail;
} else {
({fileExt, outpoint} = claimData);
obj.sourceUrl = `${createCanonicalLink({ asset: claimData })}.${fileExt}?${outpoint}`;
}
}
}
return obj;
};
const mapDispatchToProps = dispatch => {

View file

@ -81,53 +81,70 @@ class Dropzone extends React.Component {
}
}
render () {
const { dragOver, mouseOver, dimPreview } = this.state;
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
return (
<div className='dropzone-wrapper'>
<form>
<input
className='input-file'
type='file'
id='file_input'
name='file_input'
accept='video/*,image/*'
onChange={this.handleFileInput}
encType='multipart/form-data'
/>
</form>
<div
className={'dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')}
onDrop={this.handleDrop}
onDragOver={this.handleDragOver}
onDragEnd={this.handleDragEnd}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClick}>
{this.props.file ? (
<div className={'dropzone-preview-wrapper'}>
<DropzonePreviewImage
dimPreview={this.state.dimPreview}
file={this.props.file}
thumbnail={this.props.thumbnail}
<div>
{isUpdate && fileExt === 'mp4' ? (
<p>Video updates are currently disabled. This feature will be available soon. You can edit metadata.</p>
) : (
<div className='dropzone-wrapper'>
<form>
<input
className='input-file'
type='file'
id='file_input'
name='file_input'
accept='video/*,image/*'
onChange={this.handleFileInput}
encType='multipart/form-data'
/>
<div className={'dropzone-preview-overlay'}>
{ this.state.dragOver ? <DropzoneDropItDisplay /> : null }
{ this.state.mouseOver ? (
</form>
<div
className={'dropzone' + (dragOver ? ' dropzone--drag-over' : '')}
onDrop={this.handleDrop}
onDragOver={this.handleDragOver}
onDragEnd={this.handleDragEnd}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClick}>
{file || isUpdate ? (
<div className={'dropzone-preview-wrapper'}>
{file ? (
<DropzonePreviewImage
dimPreview={dimPreview}
file={file}
thumbnail={thumbnail}
/>
) : (
<DropzonePreviewImage
dimPreview
isUpdate
sourceUrl={sourceUrl}
/>
)}
<div className={'dropzone-preview-overlay'}>
{ dragOver ? <DropzoneDropItDisplay /> : null }
{ mouseOver ? (
<DropzoneInstructionsDisplay
fileError={fileError}
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
/>
) : null }
</div>
</div>
) : (
dragOver ? <DropzoneDropItDisplay /> : (
<DropzoneInstructionsDisplay
fileError={this.props.fileError}
fileError={fileError}
/>
) : null }
</div>
)
)}
</div>
) : (
this.state.dragOver ? <DropzoneDropItDisplay /> : (
<DropzoneInstructionsDisplay
fileError={this.props.fileError}
/>
)
)}
</div>
</div>
)}
</div>
);
}

View file

@ -1,16 +1,21 @@
import { connect } from 'react-redux';
import { clearFile, startPublish } from '../../actions/publish';
import { clearFile, startPublish, abandonClaim } from '../../actions/publish';
import { selectAsset } from '../../selectors/show';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
const mapStateToProps = ({ show, publish }) => {
return {
file: publish.file,
isUpdate: publish.isUpdate,
hasChanged: publish.hasChanged,
asset: selectAsset(show),
};
};
const mapDispatchToProps = {
clearFile,
startPublish,
abandonClaim,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,35 +1,76 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import {Link, withRouter} from 'react-router-dom';
import PublishUrlInput from '@containers/PublishUrlInput';
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
import ChannelSelect from '@containers/ChannelSelect';
import Row from '@components/Row';
import Label from '@components/Label';
import RowLabeled from '@components/RowLabeled';
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
import ButtonTertiary from '@components/ButtonTertiary';
import ButtonSecondary from '@components/ButtonSecondary';
import SpaceAround from '@components/SpaceAround';
import PublishFinePrint from '@components/PublishFinePrint';
import { SAVE } from '../../constants/confirmation_messages';
class PublishDetails extends React.Component {
constructor (props) {
super(props);
this.onPublishSubmit = this.onPublishSubmit.bind(this);
this.abandonClaim = this.abandonClaim.bind(this);
this.onCancel = this.onCancel.bind(this);
}
onPublishSubmit () {
this.props.startPublish(this.props.history);
}
abandonClaim () {
const {asset, history} = this.props;
if (asset) {
const {claimData} = asset;
this.props.abandonClaim({claimData, history});
}
}
onCancel () {
const { isUpdate, hasChanged, clearFile, history } = this.props;
if (isUpdate || !hasChanged) {
history.push('/');
} else {
if (confirm(SAVE)) {
clearFile();
}
}
}
render () {
const {file, isUpdate, asset} = this.props;
return (
<div>
<Row>
<PublishUrlInput />
</Row>
{isUpdate ? (asset && (
<Row>
<RowLabeled
label={
<Label value={'Channel:'} />
}
content={
<span className='text'>
{asset.claimData.channelName}
</span>
}
/>
</Row>
)) : (
<React.Fragment>
<Row>
<PublishUrlInput />
</Row>
<Row>
<ChannelSelect />
</Row>
<Row>
<ChannelSelect />
</Row>
</React.Fragment>
)}
{ this.props.file.type === 'video/mp4' && (
{ file && file.type === 'video/mp4' && (
<Row>
<PublishThumbnailInput />
</Row>
@ -41,16 +82,27 @@ class PublishDetails extends React.Component {
<Row>
<ButtonPrimaryJumbo
value={'Publish'}
value={isUpdate ? 'Update' : 'Publish'}
onClickHandler={this.onPublishSubmit}
/>
</Row>
{isUpdate && (
<Row>
<SpaceAround>
<ButtonSecondary
value={'Abandon Claim'}
onClickHandler={this.abandonClaim}
/>
</SpaceAround>
</Row>
)}
<Row>
<SpaceAround>
<ButtonTertiary
value={'Cancel'}
onClickHandler={this.props.clearFile}
onClickHandler={this.onCancel}
/>
</SpaceAround>
</Row>

View file

@ -8,6 +8,7 @@ const mapStateToProps = ({ publish }) => {
description : publish.metadata.description,
license : publish.metadata.license,
nsfw : publish.metadata.nsfw,
isUpdate : publish.isUpdate,
};
};

View file

@ -26,27 +26,30 @@ class PublishMetadataInputs extends React.Component {
this.props.onMetadataChange(name, selectedOption);
}
render () {
const { showMetadataInputs, description, isUpdate, nsfw } = this.props;
return (
<div>
{this.props.showMetadataInputs && (
{(showMetadataInputs || isUpdate) && (
<div>
<PublishDescriptionInput
description={this.props.description}
description={description}
handleInput={this.handleInput}
/>
<PublishLicenseInput
handleSelect={this.handleSelect}
/>
<PublishNsfwInput
nsfw={this.props.nsfw}
nsfw={nsfw}
handleInput={this.handleInput}
/>
</div>
)}
<ButtonSecondary
value={this.props.showMetadataInputs ? 'less' : 'more'}
onClickHandler={this.toggleShowInputs}
/>
{!isUpdate && (
<ButtonSecondary
value={showMetadataInputs ? 'less' : 'more'}
onClickHandler={this.toggleShowInputs}
/>
)}
</div>
);
}

View file

@ -12,7 +12,7 @@ class PublishStatus extends React.Component {
{status === publishStates.LOAD_START &&
<div className={'status'}>
<Row>
<p>le is loading to server</p>
<p>File is loading to server</p>
</Row>
<Row>
<p className={'text--secondary'}>0%</p>
@ -42,7 +42,7 @@ class PublishStatus extends React.Component {
</Row>
</div>
}
{status === publishStates.SUCCESS &&
{status === publishStates.SUCCEEDED &&
<div className={'status'}>
<Row>
<p>Your publish is complete! You are being redirected to it now.</p>
@ -71,6 +71,13 @@ class PublishStatus extends React.Component {
</Row>
</div>
}
{status === publishStates.ABANDONING &&
<div className={'status'}>
<Row>
<p>Your claim is being abandoned.</p>
</Row>
</div>
}
</div>
);
}

View file

@ -1,11 +1,22 @@
import {connect} from 'react-redux';
import View from './view';
import {selectAsset} from "../../selectors/show";
import {buildURI} from "../../utils/buildURI";
const mapStateToProps = ({ publish }) => {
const mapStateToProps = props => {
const { show, publish } = props;
const asset = selectAsset(show);
let uri;
if (asset) {
uri = `lbry://${buildURI(asset)}`;
}
return {
disabled: publish.disabled,
file : publish.file,
status : publish.status.status,
file: publish.file,
status: publish.status.status,
isUpdate: publish.isUpdate,
hasChanged: publish.hasChanged,
uri,
};
};

View file

@ -1,23 +1,34 @@
import React from 'react';
import { withRouter, Prompt } from 'react-router';
import Dropzone from '@containers/Dropzone';
import PublishPreview from '@components/PublishPreview';
import PublishStatus from '@containers/PublishStatus';
import PublishDisabledMessage from '@containers/PublishDisabledMessage';
import { SAVE } from '../../constants/confirmation_messages';
class PublishTool extends React.Component {
render () {
if (this.props.disabled) {
const {disabled, file, isUpdate, hasChanged, uri, status, location: currentLocation} = this.props;
if (disabled) {
return (
<PublishDisabledMessage />
);
} else {
if (this.props.file) {
if (this.props.status) {
if (file || isUpdate) {
if (status) {
return (
<PublishStatus />
);
} else {
return <PublishPreview />;
return (
<React.Fragment>
<Prompt
when={hasChanged}
message={(location) => location.pathname === currentLocation.pathname ? false : SAVE}
/>
<PublishPreview isUpdate={isUpdate} uri={uri} />
</React.Fragment>
);
}
}
return <Dropzone />;
@ -25,4 +36,4 @@ class PublishTool extends React.Component {
}
};
export default PublishTool;
export default withRouter(PublishTool);

View file

@ -1,4 +1,5 @@
import React from 'react';
import { withRouter } from 'react-router';
import PageLayout from '@components/PageLayout';
import HorizontalSplit from '@components/HorizontalSplit';
import AboutSpeechOverview from '@components/AboutSpeechOverview';
@ -20,4 +21,4 @@ class AboutPage extends React.Component {
}
}
export default AboutPage;
export default withRouter(AboutPage);

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { setUpdateTrue, setHasChanged, updateMetadata, clearFile } from '../../actions/publish';
import { onHandleShowPageUri } from '../../actions/show';
import { selectAsset } from '../../selectors/show';
import View from './view';
const mapStateToProps = (props) => {
const { show } = props;
return {
asset : selectAsset(show),
myChannel: props.channel.loggedInChannel.name,
isUpdate : props.publish.isUpdate,
};
};
const mapDispatchToProps = {
updateMetadata,
onHandleShowPageUri,
setUpdateTrue,
setHasChanged,
clearFile,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,43 @@
import React from 'react';
import PageLayout from '@components/PageLayout';
import { Redirect } from 'react-router-dom';
import PublishTool from '@containers/PublishTool';
class EditPage extends React.Component {
componentDidMount () {
const {asset, match, onHandleShowPageUri, setUpdateTrue, setHasChanged, updateMetadata} = this.props;
onHandleShowPageUri(match.params);
setUpdateTrue();
if (asset) {
['title', 'description', 'license', 'nsfw'].forEach(meta => updateMetadata(meta, asset.claimData[meta]));
}
setHasChanged(false);
}
componentWillUnmount () {
this.props.clearFile();
}
render () {
const { myChannel, asset } = this.props;
// redirect if user does not own this claim
if (
!myChannel || (
asset &&
asset.claimsData &&
asset.claimsData.channelName &&
asset.claimsData.channelName !== myChannel
)
) {
return (<Redirect to={'/'} />);
}
return (
<PageLayout
pageTitle={'Edit claim'}
pageUri={'edit'}
>
<PublishTool />
</PageLayout>
);
}
};
export default EditPage;

View file

@ -1,17 +1,20 @@
import { connect } from 'react-redux';
import { onHandleShowHomepage } from '../../actions/show';
import { clearFile } from '../../actions/publish';
import View from './view';
const mapStateToProps = ({ show, site, channel }) => {
const mapStateToProps = ({ show, site, channel, publish }) => {
return {
error : show.request.error,
requestType: show.request.type,
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
isUpdate : publish.isUpdate,
};
};
const mapDispatchToProps = {
onHandleShowHomepage,
clearFile,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -4,16 +4,9 @@ import PublishTool from '@containers/PublishTool';
import ContentPageWrapper from '@pages/ContentPageWrapper';
class HomePage extends React.Component {
componentDidMount () {
this.props.onHandleShowHomepage(this.props.match.params);
componentWillUnmount () {
this.props.clearFile();
}
componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) {
this.props.onHandleShowHomepage(nextProps.match.params);
}
}
render () {
const { homeChannel } = this.props;
return homeChannel ? (

View file

@ -1,20 +1,10 @@
import { connect } from 'react-redux';
import { selectAsset } from '../../selectors/show';
import View from './view';
const mapStateToProps = ({ show }) => {
// select request info
const requestId = show.request.id;
// select asset info
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
};
// return props
return {
asset,
asset: selectAsset(show),
};
};

View file

@ -1,6 +1,5 @@
import React from 'react';
import PageLayout from '@components/PageLayout';
import HorizontalSplit from '@components/HorizontalSplit';
import AssetTitle from '@containers/AssetTitle';
import AssetDisplay from '@containers/AssetDisplay';

View file

@ -41,6 +41,8 @@ const initialState = {
license : '',
nsfw : false,
},
isUpdate: false,
hasChanged: false,
thumbnail: null,
thumbnailChannel,
thumbnailChannelId,
@ -49,8 +51,9 @@ const initialState = {
export default function (state = initialState, action) {
switch (action.type) {
case actions.FILE_SELECTED:
return Object.assign({}, initialState, { // note: clears to initial state
return Object.assign({}, state.isUpdate ? state : initialState, { // note: clears to initial state
file: action.data,
hasChanged: true,
});
case actions.FILE_CLEAR:
return initialState;
@ -59,14 +62,17 @@ export default function (state = initialState, action) {
metadata: Object.assign({}, state.metadata, {
[action.data.name]: action.data.value,
}),
hasChanged: true,
});
case actions.CLAIM_UPDATE:
return Object.assign({}, state, {
claim: action.data,
hasChanged: true,
});
case actions.SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, {
publishInChannel: action.channel,
hasChanged: true,
});
case actions.PUBLISH_STATUS_UPDATE:
return Object.assign({}, state, {
@ -83,13 +89,26 @@ export default function (state = initialState, action) {
selectedChannel: action.data,
});
case actions.TOGGLE_METADATA_INPUTS:
return Object.assign({}, state, {
return {
...state,
showMetadataInputs: action.data,
});
};
case actions.THUMBNAIL_NEW:
return Object.assign({}, state, {
return {
...state,
thumbnail: action.data,
});
hasChanged: true,
};
case actions.SET_UPDATE_TRUE:
return {
...state,
isUpdate: true,
};
case actions.SET_HAS_CHANGED:
return {
...state,
hasChanged: action.data,
};
default:
return state;
}

View file

@ -65,6 +65,43 @@ export default function (state = initialState, action) {
},
}),
});
case actions.ASSET_REMOVE:
const claim = action.data;
const newAssetList = state.assetList;
delete newAssetList[`a#${claim.name}#${claim.claimId}`];
const channelId = `c#${claim.channelName}#${claim.certificateId}`;
const channelClaims = state.channelList[channelId].claimsData.claims;
const newClaimsData = channelClaims.filter(c => c.claimId !== claim.claimId);
return {
...state,
assetList : newAssetList,
channelList: {
...state.channelList,
[channelId]: {
...state.channelList[channelId],
claimsData: {
...state.channelList[channelId].claimsData,
claims: newClaimsData,
},
},
},
};
case actions.ASSET_UPDATE_CLAIMDATA:
return {
...state,
assetList: {
...state.assetList,
[action.data.id]: {
...state.assetList[action.data.id],
claimData: {
...state.assetList[action.data.id].claimData,
...action.data.claimData,
},
},
},
};
// channel data
case actions.CHANNEL_ADD:
return Object.assign({}, state, {
@ -77,7 +114,7 @@ export default function (state = initialState, action) {
},
}),
});
case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS:
case actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED:
return Object.assign({}, state, {
channelList: Object.assign({}, state.channelList, {
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {

View file

@ -0,0 +1,30 @@
import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../constants/publish_action_types';
import * as publishStates from '../constants/publish_claim_states';
import { updatePublishStatus, clearFile } from '../actions/publish';
import { removeAsset } from '../actions/show';
import { doAbandonClaim } from '../api/assetApi';
function * abandonClaim (action) {
const { claimData, history } = action.data;
const { claimId } = claimData;
const confirm = window.confirm('Are you sure you want to abandon this claim? This action cannot be undone.');
if (!confirm) return;
yield put(updatePublishStatus(publishStates.ABANDONING, 'Your claim is being abandoned...'));
try {
yield call(doAbandonClaim, claimId);
} catch (error) {
return console.log('abandon error:', error.message);
}
yield put(clearFile());
yield put(removeAsset(claimData));
return history.push('/');
}
export function * watchAbandonClaim () {
yield takeLatest(actions.ABANDON_CLAIM, abandonClaim);
};

View file

@ -5,37 +5,57 @@ import { updateError, updatePublishStatus, clearFile } from '../actions/publish'
import { selectPublishState } from '../selectors/publish';
import { selectChannelState } from '../selectors/channel';
import { selectSiteState } from '../selectors/site';
import { selectShowState, selectAsset } from '../selectors/show';
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
import { makePublishRequestChannel } from '../channels/publish';
function * publishFile (action) {
const { history } = action.data;
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: publishToolErrors } = yield select(selectPublishState);
const publishState = yield select(selectPublishState);
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, isUpdate, error: publishToolErrors } = publishState;
const { loggedInChannel } = yield select(selectChannelState);
const { host } = yield select(selectSiteState);
let show, asset;
if (isUpdate) {
show = yield select(selectShowState);
asset = selectAsset(show);
}
// validate the channel selection
try {
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
} catch (error) {
return yield put(updateError('channel', error.message));
};
}
// validate publish parameters
try {
validateNoPublishErrors(publishToolErrors);
} catch (error) {
return console.log('publish error:', error.message);
}
let publishMetadata, publishFormData, publishChannel;
// create metadata
let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
publishMetadata = createPublishMetadata(
isUpdate ? asset.name : claim,
isUpdate ? {type: asset.claimData.contentType} : file,
metadata,
publishInChannel,
selectedChannel
);
if (isUpdate) {
publishMetadata['channelName'] = asset.claimData.channelName;
}
if (thumbnail) {
// add thumbnail to publish metadata
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
}
// create form data for main publish
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
publishChannel = yield call(makePublishRequestChannel, publishFormData, isUpdate);
while (true) {
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
if (publishError) {
@ -43,7 +63,21 @@ function * publishFile (action) {
}
if (success) {
yield put(clearFile());
return history.push(`/${success.data.claimId}/${success.data.name}`);
if (isUpdate) {
yield put({
type: 'ASSET_UPDATE_CLAIMDATA',
data: {
id : `a#${success.data.name}#${success.data.claimId}`,
claimData: success.data.claimData,
},
});
}
if (success.data.claimId) {
return history.push(success.data.pushTo);
} else {
// this returns to the homepage, needs work
return yield put(updatePublishStatus(publishStates.FAILED, 'ERROR'));
}
}
if (loadStart) {
yield put(updatePublishStatus(publishStates.LOAD_START, null));
@ -55,7 +89,7 @@ function * publishFile (action) {
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
}
}
};
}
export function * watchPublishStart () {
yield takeLatest(actions.PUBLISH_START, publishFile);

View file

@ -10,6 +10,7 @@ import { watchUpdateChannelAvailability } from './updateChannelAvailability';
import { watchChannelCreate } from './createChannel';
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
import { watchChannelLogout } from './logoutChannel';
import { watchAbandonClaim } from './abandon';
export function * rootSaga () {
yield all([
@ -27,5 +28,6 @@ export function * rootSaga () {
watchChannelLoginCheck(),
watchChannelLogout(),
watchUpdateAssetViews(),
watchAbandonClaim(),
]);
}

View file

@ -1,7 +1,13 @@
export const selectAsset = (show) => {
const request = show.requestList[show.request.id];
const assetKey = request.key;
return show.assetList[assetKey];
export const selectAsset = show => {
const requestId = show.request.id;
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
}
return asset;
};
export const selectShowState = (state) => {

View file

@ -0,0 +1,10 @@
export const buildURI = asset => {
let channelName, certificateId, name, claimId;
if (asset.claimData) {
({ channelName, certificateId, name, claimId } = asset.claimData);
}
if (channelName) {
return `${channelName}:${certificateId}/${name}`;
}
return `${claimId}/${name}`;
};

View file

@ -16,7 +16,9 @@ export const createPublishMetadata = (claim, { type }, { title, description, lic
export const createPublishFormData = (file, thumbnail, metadata) => {
let fd = new FormData();
// append file
fd.append('file', file);
if (file) {
fd.append('file', file);
}
// append thumbnail
if (thumbnail) {
fd.append('thumbnail', thumbnail);
@ -31,5 +33,5 @@ export const createPublishFormData = (file, thumbnail, metadata) => {
};
export const createThumbnailUrl = (channel, channelId, claim, host) => {
return `${host}/${channel}:${channelId}/${claim}-thumb.png`;
return `${host}/${channel}:${channelId}/${claim}-thumb.jpg`;
};

View file

@ -850,7 +850,7 @@ var claimQueries = (db, table, sequelize) => ({
});
},
getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
logger$1.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return await table.findAll({
where: { name: claimName },
@ -860,7 +860,12 @@ var claimQueries = (db, table, sequelize) => ({
throw new Error('No claim(s) found with that claim name');
}
return returnShortId(result, claimId);
let list = result.map(claim => claim.dataValues);
if (pendingClaim) {
list = list.concat(pendingClaim);
}
return returnShortId(list, claimId);
});
},
@ -981,6 +986,24 @@ var claimQueries = (db, table, sequelize) => ({
});
},
resolveClaimInChannel: async (claimName, channelId) => {
logger$1.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
return table.findAll({
where: {
name: claimName,
publisher_id: channelId,
},
}).then(claimArray => {
if (claimArray.length === 0) {
return null;
} else if (claimArray.length !== 1) {
logger$1.warn(`more than one record matches ${claimName} in ${channelId}`);
}
return claimArray[0];
});
},
getOutpoint: async (name, claimId) => {
logger$1.debug(`finding outpoint for ${name}#${claimId}`);

View file

@ -49,7 +49,7 @@ export default (db, table, sequelize) => ({
});
},
getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return await table.findAll({
where: { name: claimName },
@ -59,7 +59,12 @@ export default (db, table, sequelize) => ({
throw new Error('No claim(s) found with that claim name');
}
return returnShortId(result, claimId);
let list = result.map(claim => claim.dataValues);
if (pendingClaim) {
list = list.concat(pendingClaim);
}
return returnShortId(list, claimId);
});
},
@ -180,6 +185,24 @@ export default (db, table, sequelize) => ({
});
},
resolveClaimInChannel: async (claimName, channelId) => {
logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
return table.findAll({
where: {
name: claimName,
publisher_id: channelId,
},
}).then(claimArray => {
if (claimArray.length === 0) {
return null;
} else if (claimArray.length !== 1) {
logger.warn(`more than one record matches ${claimName} in ${channelId}`);
}
return claimArray[0];
});
},
getOutpoint: async (name, claimId) => {
logger.debug(`finding outpoint for ${name}#${claimId}`);

View file

@ -5,7 +5,11 @@ const { returnPaginatedChannelClaims } = require('./channelPagination.js');
const getChannelClaims = async (channelName, channelShortId, page) => {
const channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelShortId);
const channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
let channelClaims;
if (channelId) {
channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
}
const processingChannelClaims = channelClaims ? channelClaims.map((claim) => getClaimData(claim)) : [];
const processedChannelClaims = await Promise.all(processingChannelClaims);

View file

@ -0,0 +1,44 @@
const logger = require('winston');
const db = require('server/models');
const { abandonClaim } = require('server/lbrynet');
const deleteFile = require('../publish/deleteFile.js');
const authenticateUser = require('../publish/authentication.js');
/*
route to abandon a claim through the daemon
*/
const claimAbandon = async (req, res) => {
const {claimId} = req.body;
const {user} = req;
try {
const [channel, claim] = await Promise.all([
authenticateUser(user.channelName, null, null, user),
db.Claim.findOne({where: {claimId}}),
]);
if (!claim) throw new Error('That channel does not exist');
if (!channel.channelName) throw new Error('You don\'t own this channel');
await abandonClaim({claimId});
const file = await db.File.findOne({where: {claimId}});
await Promise.all([
deleteFile(file.filePath),
db.File.destroy({where: {claimId}}),
db.Claim.destroy({where: {claimId}}),
]);
logger.debug(`Claim abandoned: ${claimId}`);
res.status(200).json({
success: true,
message: `Claim with id ${claimId} abandonded`,
});
} catch (error) {
logger.error('abandon claim error:', error);
res.status(400).json({
success: false,
message: error.message,
});
}
};
module.exports = claimAbandon;

View file

@ -1,8 +1,8 @@
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
const getClaimData = require('server/utils/getClaimData');
const fetchClaimData = require('server/utils/fetchClaimData');
const chainquery = require('chainquery');
const db = require('server/models');
/*
route to return data for a claim
@ -10,16 +10,9 @@ const db = require('server/models');
*/
const claimData = async ({ ip, originalUrl, body, params }, res) => {
const claimName = params.claimName;
let claimId = params.claimId;
if (claimId === 'none') claimId = null;
try {
let resolvedClaim = await chainquery.claim.queries.resolveClaim(claimName, claimId).catch(() => {});
if(!resolvedClaim) {
resolvedClaim = await db.Claim.resolveClaim(claimName, claimId);
}
const resolvedClaim = await fetchClaimData(params);
if (!resolvedClaim) {
return res.status(404).json({

View file

@ -11,7 +11,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw,
}
// provide default for license
if (license === null || license.trim() === '') {
license = ' '; // default to empty string
license = ''; // default to empty string
}
// create the basic publish params
const publishParams = {

View file

@ -17,6 +17,9 @@ const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
const authenticateUser = require('./authentication.js');
const chainquery = require('chainquery');
const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
const CLAIM_TAKEN = 'CLAIM_TAKEN';
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
@ -42,7 +45,25 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
});
}
// define variables
let channelName, channelId, channelPassword, description, fileName, filePath, fileExtension, fileType, gaStartTime, license, name, nsfw, thumbnail, thumbnailFileName, thumbnailFilePath, thumbnailFileType, title;
let channelName,
channelId,
channelPassword,
description,
fileName,
filePath,
fileExtension,
fileType,
gaStartTime,
license,
name,
nsfw,
thumbnail,
thumbnailFileName,
thumbnailFilePath,
thumbnailFileType,
title,
claimData,
claimId;
// record the start time of the request
gaStartTime = Date.now();
// validate the body and files of the request
@ -64,6 +85,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
};
throw error;
}
return Promise.all([
checkClaimAvailability(name),
createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId),
@ -83,19 +105,40 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
}
// publish the asset
return publish(publishParams, fileName, fileType);
return publish(publishParams, fileName, fileType, filePath);
})
.then(result => {
.then(publishResults => {
logger.info('Publish success >', publishResults);
claimData = publishResults;
({claimId} = claimData);
if (channelName) {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimData.certificateId, channelName);
} else {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimId, name, claimData).catch(error => {
return claimId.slice(0, 1);
});
}
})
.then(shortId => {
let canonicalUrl;
if (channelName) {
canonicalUrl = createCanonicalLink({ asset: { ...claimData, channelShortId: shortId } });
} else {
canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId } })
}
res.status(200).json({
success: true,
message: 'publish completed successfully',
data : {
name,
claimId : result.claim_id,
url : `${host}/${result.claim_id}/${name}`, // for backwards compatability with app
showUrl : `${host}/${result.claim_id}/${name}`,
serveUrl: `${host}/${result.claim_id}/${name}${fileExtension}`,
lbryTx : result,
claimId,
url : `${host}${canonicalUrl}`, // for backwards compatability with app
showUrl : `${host}${canonicalUrl}`,
serveUrl: `${host}${canonicalUrl}${fileExtension}`,
pushTo : canonicalUrl,
claimData,
},
});
// record the publish end time and send to google analytics

View file

@ -1,9 +1,19 @@
const path = require('path');
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
const parsePublishApiRequestFiles = ({file, thumbnail}) => {
const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => {
// make sure a file was provided
if (!file) {
if (isUpdate) {
if (thumbnail) {
const obj = {};
obj.thumbnailFileName = thumbnail.name;
obj.thumbnailFilePath = thumbnail.path;
obj.thumbnailFileType = thumbnail.type;
return obj;
}
return {};
}
throw new Error('no file with key of [file] found in request');
}
if (!file.path) {
@ -28,18 +38,24 @@ const parsePublishApiRequestFiles = ({file, thumbnail}) => {
if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name');
}
// validate the file
validateFileTypeAndSize(file);
if (file) validateFileTypeAndSize(file);
// return results
return {
fileName : file.name,
filePath : file.path,
fileExtension : path.extname(file.path),
fileType : file.type,
thumbnailFileName: (thumbnail ? thumbnail.name : null),
thumbnailFilePath: (thumbnail ? thumbnail.path : null),
thumbnailFileType: (thumbnail ? thumbnail.type : null),
const obj = {
fileName : file.name,
filePath : file.path,
fileExtension: path.extname(file.path),
fileType : file.type,
};
if (thumbnail) {
obj.thumbnailFileName = thumbnail.name;
obj.thumbnailFilePath = thumbnail.path;
obj.thumbnailFileType = thumbnail.type;
}
return obj;
};
module.exports = parsePublishApiRequestFiles;

View file

@ -1,81 +1,72 @@
const logger = require('winston');
const { publishClaim } = require('../../../../lbrynet');
const db = require('../../../../models');
const { publishClaim } = require('../../../../lbrynet');
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
const deleteFile = require('./deleteFile.js');
const publish = (publishParams, fileName, fileType) => {
return new Promise((resolve, reject) => {
let publishResults, certificateId, channelName;
// publish the file
return publishClaim(publishParams)
.then(result => {
logger.info(`Successfully published ${publishParams.name} ${fileName}`, result);
const publish = async (publishParams, fileName, fileType) => {
let publishResults;
let channel;
let fileRecord;
let newFile = Boolean(publishParams.file_path);
// Support new daemon, TODO: remove
publishResults = result.output && result.output.claim_id ? result.output : result;
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
return db.Channel.findOne({
where: {
channelName: publishParams.channel_name,
},
});
} else {
logger.debug('this claim was not published in a channel');
return null;
}
})
.then(channel => {
// set channel information
certificateId = null;
channelName = null;
if (channel) {
certificateId = channel.channelClaimId;
channelName = channel.channelName;
}
logger.debug(`certificateId: ${certificateId}`);
})
.then(() => {
return Promise.all([
createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults),
createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults),
]);
})
.then(([fileRecord, claimRecord]) => {
// upsert the records
const {name} = publishParams;
const {claim_id: claimId} = publishResults;
const upsertCriteria = {
name,
claimId,
};
return Promise.all([
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
]);
})
.then(([file, claim]) => {
logger.debug('File and Claim records successfully created');
return Promise.all([
file.setClaim(claim),
claim.setFile(file),
]);
})
.then(() => {
logger.debug('File and Claim records successfully associated');
// resolve the promise with the result from lbryApi publishClaim;
resolve(publishResults);
})
.catch(error => {
logger.error('PUBLISH ERROR', error);
deleteFile(publishParams.file_path); // delete the local file
reject(error);
try {
publishResults = await publishClaim(publishParams);
logger.info(`Successfully published ${publishParams.name} ${fileName}`, publishResults);
const outpoint = `${publishResults.output.txid}:${publishResults.output.nout}`;
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
channel = await db.Channel.findOne({
where: {
channelName: publishParams.channel_name,
},
});
});
} else {
channel = null;
}
const certificateId = channel ? channel.channelClaimId : null;
const channelName = channel ? channel.channelName : null;
const claimRecord = await createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults);
const {claimId} = claimRecord;
const upsertCriteria = {name: publishParams.name, claimId};
if (newFile) {
// this is the problem
//
fileRecord = await createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults);
} else {
fileRecord = await db.File.findOne({where: {claimId}}).then(result => result.dataValues);
}
const [file, claim] = await Promise.all([
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
]);
logger.info(`File and Claim records successfully created (${publishParams.name})`);
await Promise.all([
file.setClaim(claim),
claim.setFile(file),
]);
logger.info(`File and Claim records successfully associated (${publishParams.name})`);
return Object.assign({}, claimRecord, {outpoint});
} catch (err) {
// parse daemon response when err is a string
// this needs work
logger.info('publish/publish err:', err);
const error = typeof err === 'string' ? JSON.parse(err) : err;
if (publishParams.file_path) {
await deleteFile(publishParams.file_path);
}
const message = error.error && error.error.message ? error.error.message : 'Unknown publish error';
return {
error: true,
message,
};
}
};
module.exports = publish;

View file

@ -0,0 +1,199 @@
const logger = require('winston');
const db = require('server/models');
const { details, publishing: { disabled, disabledMessage, primaryClaimAddress } } = require('@config/siteConfig');
const { resolveUri } = require('server/lbrynet');
const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
const publish = require('../publish/publish.js');
const parsePublishApiRequestBody = require('../publish/parsePublishApiRequestBody');
const parsePublishApiRequestFiles = require('../publish/parsePublishApiRequestFiles.js');
const authenticateUser = require('../publish/authentication.js');
const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');
const chainquery = require('chainquery');
const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
/*
route to update a claim through the daemon
*/
const updateMetadata = ({nsfw, license, title, description}) => {
const update = {};
if (nsfw) update['nsfw'] = nsfw;
if (license) update['license'] = license;
if (title) update['title'] = title;
if (description) update['description'] = description;
return update;
};
const rando = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 6; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
return text;
};
const claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {
// logging
logger.info('Claim update request:', {
ip,
headers,
body,
files,
user,
});
// check for disabled publishing
if (disabled) {
return res.status(503).json({
success: false,
message: disabledMessage,
});
}
// define variables
let channelName,
channelId,
channelPassword,
description,
fileName,
filePath,
fileType,
gaStartTime,
thumbnail,
fileExtension,
license,
name,
nsfw,
thumbnailFileName,
thumbnailFilePath,
thumbnailFileType,
title,
claimRecord,
metadata,
publishResult,
thumbnailUpdate = false;
// record the start time of the request
gaStartTime = Date.now();
try {
({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
({fileName, filePath, fileExtension, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestFiles(files, true));
({channelName, channelId, channelPassword} = body);
} catch (error) {
return res.status(400).json({success: false, message: error.message});
}
// check channel authorization
authenticateUser(channelName, channelId, channelPassword, user)
.then(({ channelName, channelClaimId }) => {
if (!channelId) {
channelId = channelClaimId;
}
return chainquery.claim.queries.resolveClaimInChannel(name, channelClaimId).then(claim => claim.dataValues);
})
.then(claim => {
claimRecord = claim;
if (claimRecord.content_type === 'video/mp4' && files.file) {
thumbnailUpdate = true;
}
if (!files.file || thumbnailUpdate) {
return Promise.all([
db.File.findOne({ where: { name, claimId: claim.claim_id } }),
resolveUri(`${claim.name}#${claim.claim_id}`),
]);
}
return [null, null];
})
.then(([fileResult, resolution]) => {
metadata = Object.assign({}, {
title : claimRecord.title,
description: claimRecord.description,
nsfw : claimRecord.nsfw,
license : claimRecord.license,
language : 'en',
author : details.title,
}, updateMetadata({title, description, nsfw, license}));
const publishParams = {
name,
bid : '0.01',
claim_address: primaryClaimAddress,
channel_name : channelName,
channel_id : channelId,
metadata,
};
if (files.file) {
if (thumbnailUpdate) {
// publish new thumbnail
const newThumbnailName = `${name}-${rando()}`;
const newThumbnailParams = createThumbnailPublishParams(filePath, newThumbnailName, license, nsfw);
newThumbnailParams['file_path'] = filePath;
publish(newThumbnailParams, fileName, fileType);
publishParams['sources'] = resolution.claim.value.stream.source;
publishParams['thumbnail'] = `${details.host}/${newThumbnailParams.channel_name}:${newThumbnailParams.channel_id}/${newThumbnailName}-thumb.jpg`;
} else {
publishParams['file_path'] = filePath;
}
} else {
fileName = fileResult.fileName;
fileType = fileResult.fileType;
publishParams['sources'] = resolution.claim.value.stream.source;
publishParams['thumbnail'] = claimRecord.thumbnail_url;
}
const fp = files && files.file && files.file.path ? files.file.path : undefined;
return publish(publishParams, fileName, fileType, fp);
})
.then(result => {
publishResult = result;
if (channelName) {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.certificateId, channelName);
} else {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.claimId, name, result).catch(error => {
return result.claimId.slice(0, 1);
});
}
})
.then(shortId => {
let canonicalUrl;
if (channelName) {
canonicalUrl = createCanonicalLink({ asset: { ...publishResult, channelShortId: shortId } });
} else {
canonicalUrl = createCanonicalLink({ asset: { ...publishResult, shortId } })
}
if (publishResult.error) {
res.status(400).json({
success: false,
message: publishResult.message,
});
}
const {claimId} = publishResult;
res.status(200).json({
success: true,
message: 'update successful',
data : {
name,
claimId,
url : `${details.host}${canonicalUrl}`, // for backwards compatability with app
showUrl : `${details.host}${canonicalUrl}`,
serveUrl: `${details.host}${canonicalUrl}${fileExtension}`,
pushTo : canonicalUrl,
claimData: publishResult,
},
});
// record the publish end time and send to google analytics
sendGATimingEvent('end-to-end', 'update', fileType, gaStartTime, Date.now());
})
.catch(error => {
handleErrorResponse(originalUrl, ip, error, res);
});
};
module.exports = claimUpdate;

View file

@ -2,6 +2,7 @@ const axios = require('axios');
const logger = require('winston');
const { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');
const lbrynetUri = 'http://' + apiHost + ':' + apiPort;
const db = require('../models');
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');
const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');
const { publishing } = require('@config/siteConfig');
@ -46,6 +47,21 @@ module.exports = {
});
});
},
async abandonClaim ({claimId}) {
logger.debug(`lbryApi >> Abandon claim "${claimId}"`);
const gaStartTime = Date.now();
try {
const abandon = await axios.post(lbrynetUri, {
method: 'claim_abandon',
params: { claim_id: claimId },
});
sendGATimingEvent('lbrynet', 'abandonClaim', 'ABANDON_CLAIM', gaStartTime, Date.now());
return abandon.data;
} catch (error) {
logger.error(error);
return error;
}
},
getClaimList (claimName) {
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
const gaStartTime = Date.now();
@ -75,7 +91,13 @@ module.exports = {
})
.then(({ data }) => {
sendGATimingEvent('lbrynet', 'resolveUri', 'RESOLVE', gaStartTime, Date.now());
if (data.result[uri].error) { // check for errors
if (Object.keys(data.result).length === 0 && data.result.constructor === Object) {
// workaround for daemon returning empty result object
// https://github.com/lbryio/lbry/issues/1485
db.Claim.findOne({ where: { claimId: uri.split('#')[1] } })
.then(() => reject('This claim has not yet been confirmed on the LBRY blockchain'))
.catch(() => reject(`Claim ${uri} does not exist`));
} else if (data.result[uri].error) { // check for errors
reject(data.result[uri].error);
} else { // if no errors, resolve
resolve(data.result[uri]);

View file

@ -28,7 +28,7 @@ async function createFileRecordDataAfterGet (resolveResult, getResult) {
filePath,
fileType,
};
};
}
async function createFileRecordDataAfterPublish (fileName, fileType, publishParams, publishResults) {
const {

View file

@ -13,6 +13,8 @@ const claimGet = require('../../controllers/api/claim/get');
const claimList = require('../../controllers/api/claim/list');
const claimLongId = require('../../controllers/api/claim/longId');
const claimPublish = require('../../controllers/api/claim/publish');
const claimAbandon = require('../../controllers/api/claim/abandon');
const claimUpdate = require('../../controllers/api/claim/update');
const claimResolve = require('../../controllers/api/claim/resolve');
const claimShortId = require('../../controllers/api/claim/shortId');
const claimViews = require('../../controllers/api/claim/views');
@ -29,12 +31,10 @@ const getOEmbedData = require('../../controllers/api/oEmbed');
module.exports = {
// homepage routes
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
// channel routes
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
'/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] },
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
// sepcial routes
@ -47,6 +47,8 @@ module.exports = {
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
'/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get'
'/api/claim/publish': { method: 'post', controller: [ torCheckMiddleware, autoblockPublishMiddleware, multipartMiddleware, autoblockPublishBodyMiddleware, claimPublish ] },
'/api/claim/update': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimUpdate ] },
'/api/claim/abandon': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimAbandon ] },
'/api/claim/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] },
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },

View file

@ -15,6 +15,7 @@ module.exports = {
'/trending': { controller: redirect('/popular') },
'/popular': { controller: handlePageRequest },
'/new': { controller: handlePageRequest },
'/edit/:claimId': { controller: handlePageRequest },
'/multisite': { controller: handlePageRequest },
'/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest }, // for twitter
};

View file

@ -0,0 +1,25 @@
const chainquery = require('chainquery');
const db = require('server/models');
const fetchClaimData = async (params) => {
let { claimId, claimName: name } = params;
if (claimId === 'none') claimId = null;
const [cq, local] = await Promise.all([
chainquery.claim.queries.resolveClaim(name, claimId).then(res => res.dataValues).catch(() => {}),
db.Claim.resolveClaim(name, claimId).catch(() => {}),
]);
if (!cq && !local) {
return null;
}
if (cq && cq.name === name && !local) {
return cq;
}
if (local && local.name === name && !cq) {
return local;
}
return local.updatedAt > cq.modified_at ? local : cq;
};
module.exports = fetchClaimData;

View file

@ -25,7 +25,7 @@ module.exports = async (data) => {
claimId: data.claim_id || data.claimId,
fileExt: data.generated_extension || data.fileExt,
description: data.description,
thumbnail: data.generated_thumbnail || data.thumbnail,
thumbnail: data.generated_thumbnail || data.thumbnail_url || data.thumbnail,
outpoint: data.transaction_hash_id || data.outpoint,
host,
})