[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\"@babel/env\", \"@babel/react\"],\n  \"plugins\": [\"react-hot-loader/babel\", \"@babel/plugin-proposal-object-rest-spread\"]\n}\n"
  },
  {
    "path": ".eslintignore",
    "content": "client/build\nnode_modules/\npublic/bundle\nserver/render/build\nserver/bundle\ntest/\nserver/chainquery\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": [\"standard\", \"standard-jsx\"],\n  \"env\": {\n    \"es6\": true,\n    \"jest\": true,\n    \"node\": true,\n    \"browser\": true\n  },\n  \"globals\": {\n    \"GENTLY\": true\n  },\n  \"rules\": {\n    \"no-multi-spaces\": 0,\n    \"new-cap\": 0,\n    \"prefer-promise-reject-errors\": 0,\n    \"no-unused-vars\": 0,\n    \"standard/object-curly-even-spacing\": 0,\n    \"handle-callback-err\": 0,\n    \"one-var\": 0,\n    \"object-curly-spacing\": 0,\n    \"comma-dangle\": [\"error\", \"always-multiline\"],\n    \"semi\": [\"error\", \"always\", { \"omitLastInOneLineBlock\": true }],\n    \"key-spacing\": [\n      \"error\",\n      {\n        \"multiLine\": {\n          \"beforeColon\": false,\n          \"afterColon\": true\n        },\n        \"align\": {\n          \"beforeColon\": false,\n          \"afterColon\": true,\n          \"on\": \"colon\",\n          \"mode\": \"strict\"\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.log\n.idea/\n\nnode_modules\n\nclient/build\n\nsite/\n\ndevConfig/sequelizeCliConfig.js\ndevConfig/testingConfig.js\n\nserver/bundle\n\npublic/bundle/bundle.js\npublic/bundle/bundle.js.map\npublic/bundle/Lekton-*\npublic/bundle/style.css\n\nuploads\n\nconfig/\n\ndeployment-config.json\n"
  },
  {
    "path": ".npmignore",
    "content": "client/src\nserver/render/src\n.babelrc\n"
  },
  {
    "path": ".nvmrc",
    "content": "v8.12.0\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 100,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".sequelizerc",
    "content": "const path = require('path');\n\nmodule.exports = {\n  'config': path.resolve('devConfig', 'sequelizeCliConfig.js'),\n  'models-path': path.resolve('server', 'models'),\n  'seeders-path': path.resolve('server', 'seeders'),\n  'migrations-path': path.resolve('server', 'migrations')\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "dist: xenial\n#addons:\n#  apt:\n#    sources:\n#      - mysql-5.7-trusty\n#    packages:\n#      - mysql-server\n#      - mysql-client\nlanguage: node_js\nnode_js:\n  - \"lts/dubnium\"\ncache:\n  directories:\n    - \"node_modules\"\n#services:\n#  - mysql\n\njobs:\n  include:\n    - stage: \"Build\"\n      name: \"Build and run test environment\"\n\n      before_install:\n      #  - sudo mysql -e \"use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"\n      #  - sudo mysql_upgrade -u root -ppassword\n      #  - sudo service mysql restart\n      #  - mysql -u root -ppassword -e 'CREATE DATABASE IF NOT EXISTS lbry;'\n      #  - mysql -u root -ppassword -e \"CREATE USER 'lbry'@'localhost' IDENTIFIED BY 'lbry';\"\n      #  - mysql -u root -ppassword -e \"GRANT ALL ON lbry.* TO 'lbry'@'localhost';\"\n      #  - sudo service mysql restart\n        - dpkg --compare-versions `npm -v` ge 6.4.0 || npm i -g npm@^6.4.0\n\n      install:\n        - npm i\n\n      script:\n        - cp ./cli/defaults/* ./site/config/\n        - |\n          echo '{ \"sessionKey\": \"session\", \"masterPassword\": false }' > ./site/private/authConfig.json\n      #  - npm run fix\n        - npm run build\n        - npm start &\n        - sleep 10 # Attempt to collect output for 10 seconds\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017-2020 LBRY Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Spee.ch\n\nspee.ch provides a user-friendly, custom-designed, image and video hosting site backed by a decentralized network and\nblockchain ([LBRY](https://lbry.tech/)). Via just a small set of config files, you can spin your an entire spee.ch site back up including assets.\n\n**Please note: the spee.ch code base and setup instructions are no longer actively maintained now that we have lbry.tv. Proceed at your own caution. Setup will require dev ops skills.**\n\n![App GIF](https://spee.ch/e/speechgif.gif)\n\nFor a completely open, unrestricted example of a spee.ch site, check out https://www.spee.ch.\n\nFor a closed, custom-hosted and branded example, check out https://lbry.theantimedia.com/.\n\n## Installation\n\n### Ubuntu Step-by-Step\n\n[Step-by-step Ubuntu Install Guide](./docs/ubuntuinstall.md)\n\n### Full Instructions\n\n#### Get some information ready:\n\n- mysqlusername\n- mysqlpassword\n- domainname or 'http://localhost:3000'\n- speechport = 3000\n\n#### Install and Set Up Dependencies\n\n- Firewall open ports\n  - 22\n  - 80\n  - 443\n  - 3333\n  - 4444\n- [NodeJS](https://nodejs.org)\n- [MySQL version 5.7 or higher](https://dev.mysql.com/doc/refman/8.0/en/installing.html)\n  - mysqlusername or root\n  - mysqlpassword\n  - Requires mysql_native_password plugin\n  ```\n  mysql> `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';`\n  ```\n- [lbrynet](https://github.com/lbryio/lbry) daemon\n  - run this as a service exposing ports 3333 and 4444\n  - _note_: once the daemon is running, issue commands in another terminal session (tmux) to retrieve an address for your wallet to recieve 5+ LBC credits (or join us in the [#speech discord channel](https://discord.gg/YjYbwhS) and we will send you a few)\n    - `./lbrynet commands` gets a list of commands\n    - `./lbrynet account_balance` gets your balance (initially 0.0)\n    - `./lbrynet address_list` gets addresses you can use to recieve LBC\n- [FFmpeg](https://www.ffmpeg.org/download.html)\n- [ImageMagick](https://packages.ubuntu.com/xenial/graphics/imagemagick)\n- Spee.ch (below)\n- pm2 (optional) process manager such as pm2 to run speech server.js\n- http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000\n  - _note: even running on http://localhost, you must redirect http or https to port 3000_\n\n#### Clone spee.ch\n\n- release version for stable production\n\n```\n$ git clone -b release https://github.com/lbryio/spee.ch.git\n```\n\n- master version for development\n\n```\n$ git clone https://github.com/lbryio/spee.ch.git\n```\n\n- your own fork for customization\n\n#### Change directory into your project\n\n```\n$ cd spee.ch\n```\n\n#### Install node dependencies\n\n```\n$ npm install\n```\n\n#### Create the config files using the built-in CLI\n\nMake sure lbrynet is running in the background before proceeding.\n\n_note: If you are opt to run a local chainquery, such as from [lbry-docker/chainquery](https://github.com/lbryio/lbry-docker/tree/master/chainquery) you will need to specify connection details at this time in:_ ~/spee.ch/docs/setup/conf/speech/chainqueryConfig.json\n\n```\n$ npm run configure\n```\n\n#### Build & start the app\n\n```\n$ npm run build\n\n$ npm run start\n```\n\n#### View in browser\n\n- Visit [http://localhost:3000](http://localhost:3000) in your browser\n\n#### Customize your app\n\nCheck out the [customization guide](https://github.com/lbryio/spee.ch/blob/master/customize.md) to change your app's appearance and components\n\n#### (optional) add custom components and update the styles\n\n- Create custom components by creating React components in `site/custom/src/`\n- Update or override the CSS by changing the files in `site/custom/scss`\n\n#### (optional) install your own chainquery\n\nInstructions are coming at [lbry-docker] to install your own chainquery instance using docker-compose. This will require 50GB of preferably SSD space and at least 10 minutes to download, possibly much longer.\n\n## Settings\n\nThere are a number of settings available for customizing the behavior of your installation.  \n[Here](https://github.com/lbryio/spee.ch/blob/master/docs/settings.md) is some documentation on them.\n\n## API\n\n#### /api/claim/publish\n\nmethod: `POST`\n\nexample:\n\n```\ncurl -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim/publish\n```\n\nParameters:\n\n- `name` (required, must be unique across the instance)\n- `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png)\n- `nsfw` (optional)\n- `license` (optional)\n- `title` (optional)\n- `description` (optional)\n- `thumbnail` URL to thumbnail image, for .mp4 uploads only (optional)\n- `channelName` channel to publish too (optional)\n- `channelPassword` password for channel to publish too (optional, but required if `channelName` is provided)\n\nresponse:\n\n```\n{\n    \"success\": <bool>,\n    \"message\": <string>,\n    \"data\": {\n        \"name\": <string>,\n        \"claimId\": <string>,\n        \"url\": <string>,\n        \"showUrl\": <string>,\n        \"serveUrl\": <string>,\n        \"lbryTx\": {\n            \"claim_address\": <string>,\n            \"claim_id\": <string>,\n            \"fee\": <number>,\n            \"nout\": <number>,\n            \"tx\": <string>,\n            \"value\": <number>\n        }\n    }\n}\n```\n\n#### /api/claim/availability/:name\n\nmethod: `GET`\n\nexample:\n\n```\ncurl https://spee.ch/api/claim/availability/doitlive\n```\n\nresponse:\n\n```\n{\n    \"success\": <bool>,  // `true` if spee.ch successfully checked the claim availability\n    \"data\": <bool>, // `true` if claim is available, false if it is not available\n    \"message\": <string> // human readable message of whether claim was available or not\n}\n```\n\n## Contribute\n\n### Stack\n\nThe spee.ch stack is MySQL, Express.js, Node.js, and React.js. Spee.ch also runs `lbrynet` on its server, and it uses the `lbrynet` API to make requests -- such as `publish`, `create_channel`, and `get` -- on the `LBRY` network.\n\nSpee.ch also runs a sync tool, which decodes blocks from the `LBRY` blockchain as they are mined, and stores the information in MySQL. It stores all claims in the `Claims` table, and all channel claims in the `Certificates` table.\n\n- server\n  - [MySQL](https://www.mysql.com/)\n  - [express](https://www.npmjs.com/package/express)\n  - [node](https://nodejs.org/)\n  - [lbry](https://github.com/lbryio/lbry)\n  - [FFmpeg](https://www.ffmpeg.org/)\n- client\n  - [react](https://reactjs.org/)\n  - redux\n  - sagas\n  - scss\n  - handlebars\n\n### Architecture\n\n- `cli/` contains the code for the CLI tool. Running the tool will create `.json` config files and place them in the `site/config/` folder\n\n  - `configure.js` is the entry point for the CLI tool\n  - `cli/defaults/` holds default config files\n  - `cli/questions/` holds the questions that the CLI tool asks to build the config files\n\n- `client/` contains all of the client code\n\n  - The client side of spee.ch uses `React` and `Redux`\n  - `client/src/index.js` is the entry point for the client side js. It checks for preloaded state, creates the store, and places the `<App />` component in the document.\n  - `client/src/app.js` holds the `<App />` component, which contains the routes for `react-router-dom`\n  - `client/src/` contains all of the JSX code for the app. When the app is built, the content of this folder is transpiled into the `client/build/` folder.\n    - The Redux code is broken up into `actions/` `reducers/` and `selectors/`\n    - The React components are broken up into `containers/` (components that pull props directly from the Redux store), `components/` ('dumb' components), and `pages/`\n    - spee.ch also uses sagas which are in the `sagas/` folders and `channels/`\n  - `client/scss/` contains the CSS for the project \\*\n\n- `site/custom` is a folder which can be used to override the default components in `client/`\n\n  - The folder structure mimics that of the `client/` folder\n  - to customize spee.ch, place your own components and scss in the `site/custom/src/` and `site/custom/scss` folders.\n\n- `server/` contains all of the server code\n\n  - `index.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config files.\n  - `server/routes/` contains all of the routes for the express app\n  - `server/controllers/` contains all of the controllers for all of the routes\n  - `server/models/` contains all of the models which the app uses to interact with the `MySQL` database.\n    - Spee.ch uses the [sequelize](http://docs.sequelizejs.com/) ORM for communicating with the database.\n\n- `tests/` holds the end-to-end tests for this project\n  - Spee.ch uses `mocha` with the `chai` assertion library\n  - unit tests are located inside the project in-line with the files being tested and are designated with a `xxxx.test.js` file name\n\n### Tests\n\n- This package uses `mocha` with `chai` for testing.\n- Before running tests, create a `testingConfig.js` file in `devConfig/` by copying `testingConfig.example.js`\n- To run tests:\n  - To run all tests, including those that require LBC (like publishing), simply run `npm test`\n  - To run only tests that do not require LBC, run `npm run test:no-lbc`\n\n### URL formats\n\nSpee.ch has a few types of URL formats that return different assets from the LBRY network. Below is a list of all possible URLs for the content on spee.ch. You can learn more about LBRY URLs [here](https://lbry.tech/resources/uri).\n\n- retrieve the controlling `LBRY` claim:\n  - https://spee.ch/`claim`\n  - https://spee.ch/`claim`.`ext` (serve)\n  - https://spee.ch/`claim`.`ext`&`querystring` (serve transformed)\n- retrieve a specific `LBRY` claim:\n  - https://spee.ch/`claim_id`/`claim`\n  - https://spee.ch/`claim_id`/`claim`.`ext` (serve)\n  - https://spee.ch/`claim_id`/`claim`.`ext`&`querystring` (serve transformed)\n- retrieve all contents for the controlling `LBRY` channel\n  - https://spee.ch/`@channel`\n- a specific `LBRY` channel\n  - https://spee.ch/`@channel`:`channel_id`\n- retrieve a specific claim within the controlling `LBRY` channel\n  - https://spee.ch/`@channel`/`claim`\n  - https://spee.ch/`@channel`/`claim`.`ext` (serve)\n  - https://spee.ch/`@channel`/`claim`.`ext`&`querystring` (serve)\n- retrieve a specific claim within a specific `LBRY` channel\n  - https://spee.ch/`@channel`:`channel_id`/`claim`\n  - https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve)\n  - https://spee.ch/`@channel`:`channel_id`/`claim`.`ext`&`querystring` (serve)\n- `querystring` can include the following transformation values separated by `&`\n  - h=`number` (defines height)\n  - w=`number` (defines width)\n  - t=`crop` or `stretch` (defines transformation - missing implies constrained proportions)\n\n### Dependencies\n\nSpee.ch depends on two other lbry technologies:\n\n- [chainquery](https://github.com/lbryio/chainquery) - a normalized database of the blockchain data. We've provided credentials to use a public chainquery service. You can also install it on your own server to avoid being affected by the commons.\n- [lbrynet](https://github.com/lbryio/lbry) - a daemon that handles your wallet and transactions.\n\n### Bugs\n\nIf you find a bug or experience a problem, please report your issue here on GitHub and find us in the lbry discord!\n\n## License\n\nThis project is MIT licensed. For the full license, see [LICENSE](LICENSE).\n\n## Security\n\nWe take security seriously. Please contact security@lbry.com regarding any security issues. [Our GPG key is here](https://lbry.com/faq/gpg-key) if you need it.\n\n## Contact\n\nThe primary contact for this project is [@jessopb](mailto:jessop@lbry.com).\n"
  },
  {
    "path": "changelog.md",
    "content": ""
  },
  {
    "path": "cli/configure.js",
    "content": "const inquirer = require('inquirer');\nconst fs = require('fs');\nconst Path = require('path');\nconst axios = require('axios');\nconst ip = require('ip');\nconst pwGenerator = require('generate-password');\n\nconst mysqlQuestions = require(Path.resolve(__dirname, 'questions/mysqlQuestions.js'));\nconst siteQuestions = require(Path.resolve(__dirname, 'questions/siteQuestions.js'));\n\nlet primaryClaimAddress = '';\nlet thumbnailChannelDefault = '@thumbnails';\nlet thumbnailChannel = '';\nlet thumbnailChannelId = '';\n\nconst createConfigFile = (fileName, configObject, topSecret) => {\n  // siteConfig.json , siteConfig\n  const fileLocation = topSecret\n    ? Path.resolve(__dirname, `../site/private/${fileName}`)\n    : Path.resolve(__dirname, `../site/config/${fileName}`);\n\n  const fileContents = JSON.stringify(configObject, null, 2);\n  fs.writeFileSync(fileLocation, fileContents, 'utf-8');\n  console.log(`Successfully created ${fileLocation}\\n`);\n};\n\n// import existing configs or import the defaults\nlet mysqlConfig;\ntry {\n  mysqlConfig = require('../site/config/mysqlConfig.json');\n} catch (error) {\n  mysqlConfig = require('./defaults/mysqlConfig.json');\n}\nconst { database: mysqlDatabase, username: mysqlUsername, password: mysqlPassword } = mysqlConfig;\n\nlet siteConfig;\ntry {\n  siteConfig = require('../site/config/siteConfig.json');\n} catch (error) {\n  siteConfig = require('./defaults/siteConfig.json');\n}\nconst {\n  details: { port, title, host },\n  publishing: { uploadDirectory, channelClaimBidAmount: channelBid },\n} = siteConfig;\n\nlet lbryConfig;\ntry {\n  lbryConfig = require('../site/config/lbryConfig.json');\n} catch (error) {\n  lbryConfig = require('./defaults/lbryConfig.json');\n}\n\nlet loggerConfig;\ntry {\n  loggerConfig = require('../site/config/loggerConfig.json');\n} catch (error) {\n  loggerConfig = require('./defaults/loggerConfig.json');\n}\n\nlet slackConfig;\ntry {\n  slackConfig = require('../site/config/slackConfig.json');\n} catch (error) {\n  slackConfig = require('./defaults/slackConfig.json');\n}\n\nlet chainqueryConfig;\ntry {\n  chainqueryConfig = require('../site/config/chainqueryConfig.json');\n} catch (error) {\n  chainqueryConfig = require('./defaults/chainqueryConfig.json');\n}\n\n// authConfig\nlet randSessionKey = pwGenerator.generate({\n  length: 20,\n  numbers: true,\n});\n\nlet randMasterPass = pwGenerator.generate({\n  length: 20,\n  numbers: true,\n});\n\nlet authConfig;\ntry {\n  authConfig = require('../site/private/authConfig.json');\n} catch (error) {\n  authConfig = {\n    sessionKey: randSessionKey,\n    masterPassword: randMasterPass,\n  };\n}\n\n// ask user questions and create config files\ninquirer\n  .prompt(mysqlQuestions(mysqlDatabase, mysqlUsername, mysqlPassword))\n  .then(results => {\n    console.log('\\nCreating mysql config file...');\n    createConfigFile('mysqlConfig.json', results);\n  })\n  .then(() => {\n    // check for lbrynet connection & retrieve a default address\n    console.log('\\nRetrieving your primary claim address from LBRY...');\n    return axios\n      .post('http://localhost:5279', {\n        method: 'address_list',\n      })\n      .then(response => {\n        if (response.data) {\n          if (response.data.error) {\n            throw new Error(response.data.error.message);\n          }\n\n          primaryClaimAddress = response.data.result[0];\n          console.log('Primary claim address:', primaryClaimAddress, '!\\n');\n          siteConfig['publishing']['primaryClaimAddress'] = primaryClaimAddress;\n          return;\n        }\n        throw new Error('No data received from lbrynet');\n      })\n      .catch(error => {\n        throw error;\n      });\n  })\n  .then(() => {\n    console.log('\\nChecking to see if a LBRY channel exists for thumbnails...');\n    // see if a channel name already exists in the configs\n    const { publishing } = siteConfig;\n    thumbnailChannel = publishing.thumbnailChannel;\n    thumbnailChannelId = publishing.thumbnailChannelId;\n    console.log(`Thumbnail channel in configs: ${thumbnailChannel}#${thumbnailChannelId}.`);\n    // see if channel exists in the wallet\n    return axios\n      .post('http://localhost:5279', {\n        method: 'channel_list',\n      })\n      .then(response => {\n        if (response.data) {\n          if (response.data.error) {\n            throw new Error(response.data.error.message);\n          }\n\n          const channelList = response.data.result || [];\n          console.log('channels in this wallet:', channelList.length);\n          for (let i = 0; i < channelList.length; i++) {\n            if (channelList[i].name === thumbnailChannelDefault) {\n              const foundChannel = channelList[i];\n              console.log(`Found a thumbnail channel in wallet.`);\n              if (foundChannel.is_mine === false) {\n                console.log('Channel was not mine');\n                continue;\n              }\n              console.log('name:', foundChannel.name);\n              console.log('claim_id:', foundChannel.claim_id);\n              if (\n                foundChannel.name === thumbnailChannel &&\n                foundChannel.claim_id === thumbnailChannelId\n              ) {\n                console.log('No update to existing thumbnail config required\\n');\n              } else {\n                console.log(`Replacing thumbnail channel in config...`);\n                siteConfig['publishing']['thumbnailChannel'] = foundChannel.name;\n                siteConfig['publishing']['thumbnailChannelId'] = foundChannel.claim_id;\n                console.log(`Successfully replaced channel in config.\\n`);\n              }\n              return true;\n            }\n          }\n          console.log(`Did not find a thumbnail channel that is mine in wallet.\\n`);\n          return false;\n        }\n        throw new Error('No data received from lbrynet');\n      })\n      .catch(error => {\n        throw error;\n      });\n  })\n  .then(thumbnailChannelAlreadyExists => {\n    // exit if a channel already exists, skip this step\n    if (thumbnailChannelAlreadyExists) {\n      return;\n    }\n    // create thumbnail address\n    console.log('\\nCreating a LBRY channel to publish your thumbnails to...');\n    return axios\n      .post('http://localhost:5279', {\n        method: 'channel_new',\n        params: {\n          name: thumbnailChannelDefault,\n          bid: channelBid,\n        },\n      })\n      .then(response => {\n        if (response.data) {\n          if (response.data.error) {\n            throw new Error(response.data.error.message);\n          }\n          thumbnailChannel = thumbnailChannelDefault;\n          thumbnailChannelId = response.data.result.claim_id;\n          siteConfig['publishing']['thumbnailChannel'] = thumbnailChannel;\n          siteConfig['publishing']['thumbnailChannelId'] = thumbnailChannelId;\n          console.log(`Created channel: ${thumbnailChannel}#${thumbnailChannelId}\\n`);\n          return;\n        }\n        throw new Error('No data received from lbrynet');\n      })\n      .catch(error => {\n        throw error;\n      });\n  })\n  .then(() => {\n    return inquirer.prompt(siteQuestions(port, title, host, uploadDirectory));\n  })\n  .then(results => {\n    console.log('\\nCreating site config file...');\n    siteConfig['details']['port'] = results.port;\n    siteConfig['details']['title'] = results.title;\n    siteConfig['details']['host'] = results.host;\n    siteConfig['details']['ipAddress'] = ip.address();\n    siteConfig['publishing']['uploadDirectory'] = results.uploadDirectory;\n  })\n  .then(() => {\n    // create the config files\n    createConfigFile('siteConfig.json', siteConfig);\n    createConfigFile('lbryConfig.json', lbryConfig);\n    createConfigFile('loggerConfig.json', loggerConfig);\n    createConfigFile('slackConfig.json', slackConfig);\n    createConfigFile('chainqueryConfig.json', chainqueryConfig);\n    createConfigFile('authConfig.json', authConfig, true);\n  })\n  .then(() => {\n    console.log(\"\\nYou're all done!\");\n    console.log(\n      '\\nIt\\'s a good idea to BACK UP YOUR MASTER PASSWORD \\nin \"/site/private/authConfig.json\" so that you don\\'t lose \\ncontrol of your channel.'\n    );\n\n    console.log(\n      '\\nNext step: run \"npm run build\" (or \"npm run dev\") to compiles, and \"npm run start\" to start your server!'\n    );\n    console.log(\n      'If you want to change any settings, you can edit the files in the \"/site\" folder.'\n    );\n    process.exit(0);\n  })\n  .catch(error => {\n    if (error.code === 'ECONNREFUSED') {\n      console.log('Error: could not connect to LBRY.  Please make sure lbrynet daemon is running.');\n      process.exit(1);\n    } else {\n      console.log(error);\n      process.exit(1);\n    }\n  });\n"
  },
  {
    "path": "cli/defaults/chainqueryConfig.json",
    "content": "{\n  \"host\": \"public.chainquery.lbry.com\",\n  \"port\": \"3306\",\n  \"timeout\": 30,\n  \"database\": \"chainquery\",\n  \"username\": \"speechpublic\",\n  \"password\": \"7uITJLwZRvHBZYS3JZDykD1-7hLVkVA1jDWfcgqi6QnC\"\n}\n"
  },
  {
    "path": "cli/defaults/lbryConfig.json",
    "content": "{\n  \"apiHost\": \"localhost\",\n  \"apiPort\": \"5279\",\n  \"getTimeout\": 30\n}\n"
  },
  {
    "path": "cli/defaults/loggerConfig.json",
    "content": "{\n  \"logLevel\": \"verbose\"\n}\n"
  },
  {
    "path": "cli/defaults/mysqlConfig.json",
    "content": "{\n  \"database\": \"lbry\",\n  \"username\": \"root\",\n  \"password\": \"\"\n}\n"
  },
  {
    "path": "cli/defaults/siteConfig.json",
    "content": "{\n  \"analytics\": {\n    \"googleId\": null\n  },\n  \"assetDefaults\": {\n    \"title\": \"Default Content Title\",\n    \"description\": \"Default Content Description\",\n    \"thumbnail\": \"https://spee.ch/0e5d4e8f4086e13f5b9ca3f9648f518e5f524402/speechflag.png\"\n  },\n  \"auth\": {\n    \"sessionKey\": \"mysecretkeyword\",\n    \"masterPassword\": \"myMasterPassword\"\n  },\n  \"details\": {\n    \"port\": 3000,\n    \"title\": \"My Site\",\n    \"ipAddress\": \"\",\n    \"host\": \"https://www.example.com\",\n    \"description\": \"A decentralized hosting platform built on LBRY\",\n    \"twitter\": false,\n    \"blockListEndpoint\": \"https://api.lbry.com/file/list_blocked\"\n  },\n  \"publishing\": {\n    \"primaryClaimAddress\": null,\n    \"uploadDirectory\": \"/home/lbry/Uploads\",\n    \"thumbnailChannel\": null,\n    \"thumbnailChannelId\": null,\n    \"additionalClaimAddresses\": [],\n    \"disabled\": false,\n    \"disabledMessage\": \"Default publishing disabled message\",\n    \"closedRegistration\": false,\n    \"serveOnlyApproved\": false,\n    \"publishOnlyApproved\": false,\n    \"approvedChannels\": [],\n    \"publishingChannelWhitelist\": [],\n    \"channelClaimBidAmount\": \"0.1\",\n    \"fileClaimBidAmount\": \"0.01\",\n    \"fileSizeLimits\": {\n      \"image\": 50000000,\n      \"video\": 50000000,\n      \"audio\": 50000000,\n      \"text\": 50000000,\n      \"model\": 50000000,\n      \"application\": 50000000,\n      \"customByContentType\": {\n        \"application/octet-stream\": 50000000\n      }\n    }\n  },\n  \"serving\": {\n    \"dynamicFileSizing\": {\n      \"enabled\": true,\n      \"maxDimension\": 2000\n    },\n    \"markdownSettings\": {\n      \"skipHtmlMain\": true,\n      \"escapeHtmlMain\": true,\n      \"skipHtmlDescriptions\": true,\n      \"escapeHtmlDescriptions\": true,\n      \"allowedTypesMain\": [],\n      \"allowedTypesDescriptions\": [],\n      \"allowedTypesExample\": [\n        \"see react-markdown docs\",\n        \"root\",\n        \"text\",\n        \"break\",\n        \"paragraph\",\n        \"emphasis\",\n        \"strong\",\n        \"thematicBreak\",\n        \"blockquote\",\n        \"delete\",\n        \"link\",\n        \"image\",\n        \"linkReference\",\n        \"imageReference\",\n        \"table\",\n        \"tableHead\",\n        \"tableBody\",\n        \"tableRow\",\n        \"tableCell\",\n        \"list\",\n        \"listItem\",\n        \"heading\",\n        \"inlineCode\",\n        \"code\",\n        \"html\",\n        \"parsedHtml\"\n      ]\n    },\n    \"customFileExtensions\": {\n      \"application/x-troff-man\": \"man\",\n      \"application/x-troff-me\": \"me\",\n      \"application/x-mif\": \"mif\",\n      \"application/x-troff-ms\": \"ms\",\n      \"application/x-troff\": \"roff\",\n      \"application/x-python-code\": \"pyc\",\n      \"text/x-python\": \"py\",\n      \"application/x-pn-realaudio\": \"ram\",\n      \"application/x-sgml\": \"sgm\",\n      \"model/stl\": \"stl\",\n      \"image/pict\": \"pct\",\n      \"text/xul\": \"xul\",\n      \"text/x-go\": \"go\"\n    }\n  },\n  \"startup\": {\n    \"performChecks\": true,\n    \"performUpdates\": true\n  }\n}\n"
  },
  {
    "path": "cli/defaults/slackConfig.json",
    "content": "{\n  \"slackWebHook\": false,\n  \"slackErrorChannel\": false,\n  \"slackInfoChannel\": false\n}"
  },
  {
    "path": "cli/questions/mysqlQuestions.js",
    "content": "const database = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'What is the name of the MySQL DATABASE to be used?',\n    default: defaultAnswer,\n    name   : 'database',\n  };\n};\n\nconst username = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'What is the USER NAME for your MySQL database?',\n    default: defaultAnswer,\n    name   : 'username',\n  };\n};\n\nconst password = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'What is the PASSWORD for your MySQL database?',\n    default: defaultAnswer,\n    name   : 'password',\n  };\n};\n\nmodule.exports = (defaultDatabase, defaultUsername, defaultPassword) => {\n  return [\n    database(defaultDatabase),\n    username(defaultUsername),\n    password(defaultPassword),\n  ];\n};\n"
  },
  {
    "path": "cli/questions/siteQuestions.js",
    "content": "const makeDir = require('make-dir');\n\nconst port = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'Enter a PORT for your server to run on.',\n    default: defaultAnswer,\n    name   : 'port',\n  };\n};\n\nconst title = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'Enter a title for your site.',\n    default: defaultAnswer,\n    name   : 'title',\n  };\n};\n\nconst host = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'Enter your site\\'s domain.',\n    default: defaultAnswer,\n    name   : 'host',\n  };\n};\n\nconst uploadDirectory = (defaultAnswer) => {\n  return {\n    type   : 'input',\n    message: 'Enter a directory where uploads should be stored.',\n    default: defaultAnswer,\n    name   : 'uploadDirectory',\n    validate (input) {\n      // make sure the directory exists\n      return new Promise((resolve, reject) => {\n        console.log('\\n\\nCreating directory', input, '...');\n        try {\n          const dirPath = makeDir.sync(input);\n          console.log('Successfully created directory at', dirPath, '\\n');\n        } catch (error) {\n          console.log('Failed to create directory, please create directory manually.\\n');\n        }\n        resolve(true);\n      });\n    },\n  };\n};\n\nmodule.exports = (defaultPort, defaultTitle, defaultHost, defaultUploadDirectory) => {\n  return [\n    port(defaultPort),\n    title(defaultTitle),\n    host(defaultHost),\n    uploadDirectory(defaultUploadDirectory),\n  ];\n};\n"
  },
  {
    "path": "client/scss/_asset-blocked.scss",
    "content": ".asset-blocked__image {\nwidth: 100%;\n}\n\n.asset-blocked__text {\nwidth: 100%;\n}\n"
  },
  {
    "path": "client/scss/_asset-display.scss",
    "content": ".asset-main {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.asset-document {\n  max-width: 1000px;\n  padding: $thin-padding;\n  height: fit-content;\n  box-sizing: border-box;\n}\n.asset-display {\n  height: fit-content;\n  width: fit-content;\n}\n\n.asset-title {\n  max-width: 1000px;\n  padding-bottom: $thin-padding;\n  text-align: center;\n}\n\n.asset-image, .asset-video {\n  max-height: 75vh;\n  max-width: 85vw;\n  object-fit: contain;\n  object-position: center;\n  background: black;\n}\n\n/*below must die if this is intended to be shareable component! it also probably doesn't need to be*/\n\n.visible-content {\n  margin: 0;\n  padding-bottom: 30px;\n  position: relative;\n  width: 100%;\n\n  &.closed {\n    box-shadow: none;\n\n    &:after {\n      box-shadow: none;\n    }\n  }\n\n  &:after {\n    box-shadow: 0px 2px 3px 2px $shadow-color;\n    content: '';\n    height: 0;\n    position: absolute;\n    top: 100%;\n    width: 100%;\n    z-index: 100;\n  }\n}\n\n\n.vertical-split, .visible-content {\n  flex           : 1 0 auto;\n  display        : flex;\n  flex-direction : column;\n  justify-content: space-between;\n  align-items    : center;\n};\n\n.collapse-content {\n  flex-grow: 0;\n  @media (max-width: $break-point-tablet) {\n    max-width: 100%;\n    width: 100%;\n  }\n}\n\n.collapse-content.closed{\n  display: none;\n}\n\n.collapse-button {\n  background: none;\n  border: none;\n  display: block;\n  margin: 15px auto 0;\n  width: 25px;\n  height: 25px;\n  text-align: center;\n  padding: 0px;\n\n  @media (max-width: $break-point-tablet) {\n    padding: 0;\n  }\n\n  svg {\n    stroke: $primary-color;\n    &.plus-icon {\n      transform: rotate(0);\n      transition: all 0.4s ease;\n    }\n\n  }\n\n  &:hover {\n    .plus-icon {\n      transform: rotate(-180deg);\n    }\n  }\n}\n\n.asset-info {\n  $asset-info-width: 1000px;\n  max-width: $asset-info-width;\n  margin: $primary-padding;\n  width: 100%;\n\n  @media (min-width: $break-point-tablet) {\n    padding: $primary-padding;\n  }\n  @media (max-width: $break-point-tablet) {\n    padding: $tertiary-padding;\n  }\n\n  @media (max-width: $break-point-mobile) {\n    margin: $tertiary-padding;\n  }\n}\n\n.asset-footer {\n  border-top: $subtle-border;\n  padding-top: $primary-padding;\n  margin-top: $primary-padding;\n  color: $grey;\n  text-align: center;\n}\n"
  },
  {
    "path": "client/scss/_asset-preview.scss",
    "content": ".asset-preview {\n  display: flex;\n  flex-direction: column;\n  background: $card-color;\n  color: $text-color;\n  width: 240px;\n  border: $subtle-border;\n  height: 256px;\n  &:hover {\n    border-color: $highlight-border-color;\n    color: $primary-color;\n  }\n}\n\n.asset-preview__image {\n  height  : 180px;\n  width   : 240px;\n  overflow: hidden;\n  object-fit: cover;\n  padding: 0;\n  margin : 0;\n  box-sizing: border-box;\n\n}\n\n.asset-preview__image-box {\n  width  : 240px;\n  height : 180px;\n  padding: 0;\n  margin : 0;\n  box-sizing: border-box;\n}\n\n.asset-preview__label {\n  padding: $thin-padding;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n}\n\n.asset-preview__label-text {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n  box-sizing: border-box;\n  font-size: $text-small;\n  font-weight: bold;\n  height: 54px;\n}\n\n.asset-preview__label-info {\n  width: 100%;\n  height: 15px;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  box-sizing: border-box;\n  align-items: center;\n}\n\n.asset-preview__label-info-datum {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  overflow: hidden;\n  box-sizing: border-box;\n  font-size: $text-small;\n  max-width: 40%;\n}\n\n.asset-preview__label-info-datum svg{\n  height: 1.2em;\n  width: 1.2em;\n  padding: 0;\n  padding-right: $thin-padding;\n  margin: 0;\n}\n\n.asset-preview__label-info-datum .svg-icon{\n  padding: 0px;\n  margin: 0;\n  height: 15px;\n}\n\n.asset-preview__blocked {\n  box-sizing: border-box;\n  background: black;\n  color: white;\n  height: 64%;\n  padding: $thin-padding;\n  margin-bottom: $thin-padding;\n}\n"
  },
  {
    "path": "client/scss/_body.scss",
    "content": "body {\n  margin: 0;\n  padding: 0;\n  min-height: 100%;\n  word-wrap: break-word;\n  display: -webkit-flex;\n  display: flex;\n  -webkit-flex-direction: column;\n  flex-direction: column;\n}"
  },
  {
    "path": "client/scss/_button-primary.scss",
    "content": ".button--primary, .button--primary:focus, .button--primary:active {\n  border-color: $primary-color;\n  color: $primary-color;\n  background-color: $background-color;\n}\n\n.button--primary:hover {\n  color: $background-color;\n  background-color: $primary-color;\n}\n\n.button--primary:active {\n  $color: darken($primary-color, 10%);\n  border-color: $color;\n  background-color: $color;\n}"
  },
  {
    "path": "client/scss/_button-secondary.scss",
    "content": ".button--secondary, .button--secondary:focus, .button--secondary:active  {\n  border-bottom-color: $secondary-color;\n  color: $secondary-color;\n  background-color: $background-color;\n}\n\n.button--secondary:active {\n  $color: darken($secondary-color, 10%);\n  color: $color;\n  border-bottom-color: $color;\n}\n"
  },
  {
    "path": "client/scss/_button.scss",
    "content": "button {\n  cursor: pointer;\n  &:active\n  {\n    outline: 0;\n  }\n}\n\n.button--primary, .button--secondary\n{\n  border-width: $button-border-width;\n  border-style: $button-border-strength;\n  border-color: transparent;\n  padding: $thin-padding;\n}\n\n\n.button--jumbo, .button--jumbo:focus, .button--jumbo:active {\n  width: $button-full-width;\n  font-size: x-large;\n}\n"
  },
  {
    "path": "client/scss/_channel-claims-display.scss",
    "content": ".channel-claims-display {\n  width: 100%;\n  display: grid;\n  grid-gap: $tertiary-padding;\n  align-content: space-around;\n  @media (min-width: $break-point-x-large) {\n    grid-template-columns: 1fr 1fr 1fr 1fr 1fr;\n  }\n  @media (min-width: $break-point-large) and (max-width: $break-point-x-large){\n      grid-template-columns: 1fr 1fr 1fr 1fr;\n  }\n\n  @media (min-width: $break-point-tablet) and (max-width: $break-point-large) {\n    grid-template-columns: 1fr 1fr 1fr;\n  }\n\n  @media (min-width: $break-point-mobile) and (max-width: $break-point-tablet) {\n    grid-template-columns: 1fr 1fr;\n  }\n\n  @media (max-width: $break-point-mobile) {\n    grid-template-columns: 1fr;\n  }\n}\n"
  },
  {
    "path": "client/scss/_claim-pending.scss",
    "content": ".claim-pending {\n  display: inline-block;\n  position: absolute;\n  top: 10px;\n  left: 10px;\n  padding: 5px;\n  border-radius: 5px;\n  border: 2px solid red;\n  color: red;\n  font-weight: bold;\n  background-color: white;\n}\n"
  },
  {
    "path": "client/scss/_click-to-copy.scss",
    "content": ".click-to-copy-wrap {\n  display: flex;\n  width: 100%;\n  justify-content: space-between;\n  align-items: center;\n  cursor: pointer;\n  border: 1px solid $subtle-border-color;\n  border-radius: 6px;\n  .click-to-copy {\n    border: none;\n    padding: 0.36em 0.5em;\n    margin: 0;\n    background-color: transparent;\n    width: calc(100% - 1em - 2px);\n    font-size: 14px;\n    letter-spacing: -0.6px;\n    line-height: 20px;\n    letter-spacing: 0;\n    font-family: monospace;\n    border-right: 1px solid $subtle-border-color;\n  }\n  .icon-wrap {\n    width: 30px;\n    height: 30px;\n    line-height: 34px;\n    text-align: center;\n    svg {\n      stroke: $primary-color;\n      width: 16px;\n      height: 16px;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/_dropzone.scss",
    "content": ".dropzone-wrapper {\n  // fill the parent flex container\n  flex: 1 0 auto;\n  // be a flex container for children\n  display: flex;\n  flex-direction: column;\n  position: relative;\n  width: 100%;\n  box-sizing: border-box;\n  padding: 0px;\n}\n\n.dropzone {\n  border: 2px dashed $drop-zone-border-color;\n  // fill the parent flex container\n  flex: 1 0 auto;\n  // be a flex container for children\n  display: flex;\n  padding: $thin-padding;\n  -webkit-flex-direction: column;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  user-select: none;\n}\n\n.dropzone:hover, .dropzone--active {\n  border: 2px dashed $drop-zone-border-hover;\n  cursor: pointer;\n}\n\n.dropzone-dropit-display, .dropzone-instructions-display {\n  padding: 1em;\n  text-align: center;\n}\n\n.dropzone-dropit-display\n{\n  color: $primary-color;\n}\n\n.dropzone-preview-wrapper {\n  position: relative;\n  width: 100%;\n  .dropzone-preview-overlay {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    -webkit-flex-direction: column;\n    flex-direction: column;\n    justify-content: center;\n  }\n}\n\n.dropzone-preview-image {\n  display: block;\n  width: 100%;\n}\n\n.dropzone-preview-memeify {\n  margin-top: 3em;\n}\n\n.dropzone-memeify-button {\n  background: $primary-color;\n  color: #fff;\n  cursor: pointer;\n  font-size: .8em;\n  padding: 3px 6px;\n  position: absolute;\n  right: 0;\n  top: 0;\n  z-index: 3;\n}\n\n.dropzone-memeify-saveMessage {\n  padding-top: .25em;\n  position: relative;\n  top: .5em;\n}\n\n.dropzone-memeify-toolbar {\n  /* TODO: Cleanup `!important` */\n  background: $primary-color !important;\n  left: -1em !important;\n  right: -1em !important;\n  top: -4em !important;\n}\n\n.dropzone-instructions-display__chooser-label {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "client/scss/_form-feedback.scss",
    "content": ".form-feedback {\n  padding-top: $thin-padding;\n  padding-bottom: $thin-padding;\n}\n\n.form-feedback--default {\n  color: $secondary-color;\n}\n\n.form-feedback--success {\n  color: $success-color;\n}\n\n.form-feedback--failure {\n  color: $failure-color;\n}\n"
  },
  {
    "path": "client/scss/_form.scss",
    "content": ".form-group {\n  padding-bottom: $secondary-padding;\n}\n\n.form-title {\n  padding-bottom: $secondary-padding;\n}\n"
  },
  {
    "path": "client/scss/_horizontal-split.scss",
    "content": ".horizontal-split {\n  max-width: $width-content-constrained;\n  width: 100%;\n  display       : flex;\n  flex-direction : row;\n  justify-content: center;\n  box-sizing: border-box;\n\n  &.horizontal-split--mobile-collapse {\n    @media (max-width: $break-point-tablet) {\n      flex-direction: column;\n      .horizontal-split__column {\n      }\n      .horizontal-split__column--left {\n        padding-top: $thin-padding;\n      }\n      .horizontal-split__column--right {\n        padding-top: $thin-padding;\n      }\n    }\n  }\n};\n\n.horizontal-split__column {\n  display       : flex;\n  flex: 1 1 auto;\n  box-sizing: border-box;\n  width: 100%;\n}\n\n.horizontal-split__column--left {\n  padding: $tertiary-padding;\n\n  @media (max-width: $break-point-tablet) {\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n}\n\n.horizontal-split__column--right {\n\n  padding: $tertiary-padding;\n\n  @media (max-width: $break-point-tablet) {\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n}\n\n@media (max-width: $break-point-tablet) {\n\n  .horizontal-split__column {\n    justify-content: space-between;\n  };\n\n  .column {\n    width: 100%;\n    padding-left: 0;\n    padding-right: 0;\n    padding-bottom: $secondary-padding;\n  }\n}\n"
  },
  {
    "path": "client/scss/_html.scss",
    "content": "html {\n  margin: 0;\n  padding: 0;\n  height: 100%;\n}\n"
  },
  {
    "path": "client/scss/_input.scss",
    "content": "input:-webkit-autofill {\n  -webkit-box-shadow: 0 0 0px 1000px white inset;\n}\n\ninput {\n  margin: 0;\n  padding: $input-padding;\n  border: 0;\n  background-color: $background-color;\n  display: inline-block;\n  color: $text-color\n}\n\n.input-slider {\n  width: 100%\n}\n\n.input-checkbox {\n  border: 1px solid black;\n  background: white;\n}\n\n.input-file {\n  width: 0.1px;\n  height: 0.1px;\n  opacity: 0;\n  overflow: hidden;\n  position: absolute;\n  z-index: -1;\n}\n\n.input-radio {\n  cursor: pointer;\n}\n\n// input area wrapper\n\n.input-area {\n  border-bottom: 1px solid $secondary-color;\n}\n\n.form-group {\n  padding-bottom: $secondary-padding;\n}\n\n// modifiers\n\n.input--full-width {\n  width: $input-full-width;\n}\n"
  },
  {
    "path": "client/scss/_label.scss",
    "content": ".label {\n  padding-top: $thin-padding;\n  padding-bottom: $thin-padding;\n  display: inline-block;\n  font-weight: bold;\n  font-size: $text-medium;\n  width: 100%;\n  box-sizing: border-box;\n}\n\n.label-radio {\n  padding-left: $thin-padding;\n  padding-right: $thin-padding;\n  cursor: pointer;\n  font-weight: bold;\n}\n\n@media (max-width: $break-point-tablet ) {\n\n  // note: bolding break point lines up with row-label break point\n  .label, .label-radio {\n    font-weight: bold;\n  }\n\n}\n"
  },
  {
    "path": "client/scss/_link.scss",
    "content": "a, a:visited {\n  text-decoration: none;\n}\n\n.link--primary, .link--primary:visited {\n  color: $primary-color;\n  &:hover { text-decoration: underline; }\n}\n\n.link--nav {\n  color: $text-color;\n  &:hover {\n    color: $primary-color;\n  }\n}\n\n\n.link--nav-active {\n  border-bottom: 2px solid $primary-color;\n}\n"
  },
  {
    "path": "client/scss/_markdown.scss",
    "content": ".markdown-preview {\n\n  margin: $tertiary-padding 0px;\n\n  h1,\n  h2,\n  h3 {\n    font-size: inherit;\n    font-weight: inherit;\n    margin: $tertiary-padding 0px;\n  }\n\n  h4, h5, h6 {\n    font-size: $text-large;\n    font-weight: 600;\n    margin: $tertiary-padding 0px;\n  }\n\n  // Paragraphs\n  p {\n    font-size: 1.15rem;\n    white-space: pre-line;\n    margin: $tertiary-padding 0px;\n\n    svg {\n      width: 1rem;\n      height: 1rem;\n\n      margin-left: 0.2rem;\n      position: relative;\n      top: 1px;\n    }\n  }\n\n  blockquote {\n    background: $blockquote-background;\n    padding: $tertiary-padding;\n    min-width: 60%;\n    margin: $tertiary-padding;\n    p:first-child{\n      margin-top: 0px;\n    }\n    p:last-child {\n      margin-bottom: 0px;\n    }\n\n    div {\n      display: none;\n    }\n  }\n\n  // Strikethrough text\n  del {\n  }\n\n  // Tables\n  table {\n    width: 100%;\n    background-color: $base-color;\n    border-spacing: 0;\n    border: .5px solid $chrome-color;\n    margin: $tertiary-padding 0px;\n\n    tr {\n      td,\n      th,\n      td:first-of-type,\n      th:first-of-type,\n      td:last-of-type,\n      th:last-of-type {\n        padding: $thin-padding $tertiary-padding;\n        text-overflow: ellipsis;\n      }\n      td:last-of-type {\n        text-align: right;\n      }\n\n      th {\n        background: $chrome-color;\n      }\n\n    }\n    tr:nth-child(even){\n      background: $chrome-color;\n    }\n  }\n\n  // Image\n  img {\n    margin-bottom: $tertiary-padding;\n    margin-top: $tertiary-padding;\n    padding: $secondary-padding;\n    object-fit: scale-down;\n    box-sizing: border-box;\n    display: block;\n    margin-left: auto;\n    margin-right: auto;\n    max-width: 90vw;\n  }\n\n  iframe {\n    display: block;\n    margin-left: auto;\n    margin-right: auto;\n    max-width: 90vw;\n    margin-top: $tertiary-padding 0px;\n    margin-bottom: $tertiary-padding 0px;\n  }\n\n  // Horizontal Rule\n  hr {\n    width: 100%;\n    height: 1px;\n\n    background-color: $base-color;\n    margin-bottom: 2rem;\n    position: relative;\n    top: 1rem;\n\n    html[data-theme='dark'] & {\n      background-color: rgba($base-color, 0.2);\n    }\n  }\n\n  // Code\n  pre {\n    white-space: normal;\n  }\n\n  code {\n    margin-bottom: $tertiary-padding;\n    padding: $tertiary-padding;\n\n    background-color: $subtle-border-color;\n    color: $text-color;\n    display: block;\n    font-family: Consolas, 'Lucida Console', 'Source Sans', monospace;\n  }\n\n  a {\n    color: $primary-color;\n    display: inline;\n  }\n\n  // Lists\n  ul,\n  ol {\n    margin-bottom: $thin-padding;\n\n    > li {\n      list-style-position: outside;\n    }\n  }\n\n  ul {\n    list-style: initial;\n  }\n\n  li {\n    margin-left: $primary-padding;\n\n    p {\n      display: inline-block;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/_media-queries.scss",
    "content": "@media (max-width: $break-point-x-large) {\n  // hide site description in nav bar\n  .site-description {\n    display: none;\n  }\n\n}"
  },
  {
    "path": "client/scss/_nav-bar.scss",
    "content": ".nav-bar {\n  box-sizing: border-box;\n  padding: $thin-padding $primary-padding;\n  background: $chrome-color;\n  flex: 0 1 auto;\n  width: 100%;\n  border-bottom: $subtle-border;\n  color: $primary-color;\n\n  @media (max-width: $break-point-mobile) {\n    margin-left: 15px;\n    margin-right: 15px;\n  }\n  input {\n    background: $chrome-color;\n\n  }\n  select {\n    background: $chrome-color;\n    color: $text-color;\n  }\n}\n\n.nav-bar-link {\n  padding: calc(1em - 2px);\n  display: inline-block;\n  font-size: $text-medium;\n  letter-spacing: 0.4px;\n  text-transform: uppercase;\n}\n\n.nav-bar-logo {\n  cursor: pointer;\n}\n\n@media (max-width: $break-point-tablet ) {\n  .nav-bar-link {\n    padding-top: calc(1em - 2px);\n    padding-right: 1em;\n    padding-bottom: calc(1em - 2px);\n    padding-left: 1em;\n  }\n\n}\n\n@media (max-width: $break-point-mobile ) {\n  .nav-bar-link {\n    padding-top: calc(0.5em - 2px);\n    padding-right: 0.5em;\n    padding-bottom: calc(0.5em - 2px);\n    padding-left: 0.5em;\n  }\n}\n"
  },
  {
    "path": "client/scss/_page-content.scss",
    "content": ".page-content {\n  margin: $primary-padding;\n  // fill the parent flex container\n  flex: 1 0 auto;\n  // be a flex container for children\n  display: flex;\n  -webkit-flex-direction: column;\n  flex-direction: column;\n};\n"
  },
  {
    "path": "client/scss/_page-layout-show-lite.scss",
    "content": ".page-layout-show-lite {\n  flex: 1 0 auto;\n  display: flex;\n  flex-direction: column;\n  .content {\n    flex: 1 0 auto;\n    display: flex;\n    flex-direction: column;\n  }\n  .footer {\n    flex: 0 1 auto;\n  }\n}\n"
  },
  {
    "path": "client/scss/_page-layout.scss",
    "content": ".page-layout {\n  flex: 1 0 auto;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  max-width: 100%;\n  .content {\n    flex: 1 0 auto;\n    display: flex;\n    -webkit-flex-direction: column;\n    flex-direction: column;\n    width: 100%;\n    align-items: center;\n    box-sizing: border-box;\n    background: $base-color;\n\n    @media (min-width: $break-point-tablet) {\n      padding: $primary-padding;\n    }\n\n    @media (max-width: $break-point-tablet) {\n      padding: $tertiary-padding;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/_progress-bar.scss",
    "content": ".progress-bar__wrapper {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.progress-bar--inactive {\n  color: $grey;\n}\n\n.progress-bar--active {\n  color: $primary-color;\n}\n"
  },
  {
    "path": "client/scss/_publish-disabled-message.scss",
    "content": ".publish-disabled-message {\n  // fill the parent flex container\n  flex: 1 0 auto;\n  // be a flex container for children\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  .message {\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "client/scss/_publish-preview.scss",
    "content": ".publish-form__title {\n  max-width: $width-content-constrained;\n  margin-left: auto;\n  margin-right: auto;\n\n  @media (max-width: $break-point-mobile) {\n    font-size: .8em;\n  }\n}\n\n.publish-preview-dim {\n  opacity: 0.2;\n}\n"
  },
  {
    "path": "client/scss/_publish-status.scss",
    "content": ".publish-status {\n  // fill the parent flex container\n  flex: 1 0 auto;\n  // be a flex container for children\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  .status {\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "client/scss/_publish-url-input.scss",
    "content": ".publish-url-input {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  align-items: baseline;\n  border-bottom: solid 1px grey;\n  .shrink {\n    flex: 0 1 auto;\n  };\n  .fill {\n    flex: 1 0 auto;\n  };\n}\n\n\n.publish-url-text {\n  margin: 0;\n  padding: 0;\n  color: $help-color;\n}\n"
  },
  {
    "path": "client/scss/_react-app.scss",
    "content": "#react-app {\n  flex: 1 0 auto;\n  display: -webkit-flex;\n  display: flex;\n  -webkit-flex-direction: column;\n  flex-direction: column;\n}\n"
  },
  {
    "path": "client/scss/_reset.scss",
    "content": "button, input, textarea, label, select, option {\n  font-family: inherit;\n  font-size: inherit;\n}"
  },
  {
    "path": "client/scss/_row.scss",
    "content": ".row {\n  margin-bottom: 1.2em;\n}\n\n.row-labeled {\n  display: flex;\n  flex-direction: column;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  padding-bottom: $tertiary-padding;\n}\n\n.row-labeled-label {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  flex: 1;\n}\n.row-labeled-content {\n  align-self: center;\n  width: 100%;\n}\n\n@media (max-width: $break-point-tablet ) {\n  .row-labeled {\n    flex-direction: column;\n  }\n  .row-labeled-label {\n    width: 100%;\n  }\n  .row-labeled-content {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "client/scss/_select.scss",
    "content": "select {\n  margin: 0;\n  display: inline-block;\n  background: $background-color;\n  border: 0;\n  color: $text-color;\n}\n"
  },
  {
    "path": "client/scss/_share-buttons.scss",
    "content": ".share-buttons {\n  display: flex;\n  align-items: center;\n  \n  a {\n    display: block;\n    width: 30px;\n    height: 30px;\n    margin: 0 7px;\n    border-radius: 100%;\n    line-height: 30px;\n    text-align: center;\n    transition: all 0.2s ease;\n    &.twitter {\n      background:#4DC2FE;\n      img {\n        margin-top: 8px;\n        margin-left: 2px;\n      }\n    }\n    \n    &.facebook {\n      background: #5487DE;\n      img {\n        margin-top: 6px;\n      }\n    }\n    \n    &.tumblr {\n      background: #274061;\n      img {\n        margin-top: 7px;\n      }\n    }\n    \n    &.reddit {\n      background: #FF4500;\n      img {\n        margin-top: 7px;\n      }\n    }\n    \n    &:first-child{\n      margin-left: 0px;\n    }\n    \n    &:hover {\n      background: $primary-color;\n    }\n  }\n}\n"
  },
  {
    "path": "client/scss/_social-share-link.scss",
    "content": ".social-share-link {\n  flex-wrap: wrap;\n  margin-right: -0.5em;\n  margin-left: -0.5em;\n}\n\n.social-share-link > a{\n  padding-right:0.5em;\n  padding-left:0.5em;\n}"
  },
  {
    "path": "client/scss/_space-around.scss",
    "content": ".space-around {\n  display: flex;\n  justify-content: space-around;\n  align-items: center;\n}\n"
  },
  {
    "path": "client/scss/_space-between.scss",
    "content": ".space-between {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n"
  },
  {
    "path": "client/scss/_text.scss",
    "content": "// set defaults\n\nh1, h2, h3, h4, p {\n  margin: 0;\n}\n\nbody {\n  color: $text-color;\n  font-family: 'Circular', serif;\n  font-size: 14px;\n}\n\nbody a {\n  color: $primary-color;\n}\n\nh1 {\n font-size: $text-xx-large;\n}\n\nh2 {\n  font-size: $text-x-large;\n}\n\nh3 {\n  font-size: $text-large;\n}\n\n.text--extra-large {\n  font-size: $text-xx-large;\n}\n\n.text--large {\n  font-size: $text-large;\n}\n\n.text--medium {\n  font-size: $text-medium;\n}\n\n.text--small {\n  font-size: $text-small;\n}\n\n.text--extra-small {\n  font-size: $text-x-small;\n}\n\n.text--secondary {\n  color: $help-color;\n}\n\n.text--interactive {\n  color: $primary-color;\n}\n\n.text--failure {\n  color: $failure-color;\n}\n\n.text--success {\n  color: $success-color;\n}\n"
  },
  {
    "path": "client/scss/_textarea.scss",
    "content": "textarea {\n  margin: 0;\n  padding: $input-padding;\n  display: inline-block;\n  width: $input-full-width;\n}\n"
  },
  {
    "path": "client/scss/_tooltip.scss",
    "content": "/* Tooltip container */\n.tooltip {\n  position: relative;\n}\n/* Tooltip text */\n.tooltip > .tooltip-text {\n  visibility: hidden;\n  width: 15em;\n  background-color: #9b9b9b;\n  color: #fff;\n  text-align: center;\n  padding: 0.5em;\n  /* Position the tooltip text */\n  position: absolute;\n  z-index: 1;\n  bottom: 110%;\n  left: 50%;\n  margin-left: -8em; /* Use half of the width (120/2 = 60), to center the tooltip */\n}\n/* Show the tooltip text when you mouse over the tooltip container */\n.tooltip:hover > .tooltip-text {\n  visibility: visible;\n}\n/* arrow at bottom of tooltip text */\n.tooltip > .tooltip-text::after {\n  content: \" \";\n  position: absolute;\n  top: 100%;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px;\n  border-style: solid;\n  border-color: #9b9b9b transparent transparent transparent;\n}\n"
  },
  {
    "path": "client/scss/_variables.scss",
    "content": "//backgrounds\n$base-color: white; //default white\n$card-color: white; //default white\n$chrome-color: white; //default white (navbar)\n$blockquote-background: #EEEEFF;\n$background-color: $base-color;\n\n//text colors\n$primary-color: #005da0; //link default light blue #005da0\n$secondary-color: $primary-color;\n$text-color: #333;\n$success-color: green;\n$failure-color: red;\n$grey: #9095A5;\n$blockquote-text: $text-color;\n\n\n//borders and highlights\n$grey: #9095A5;\n$help-color: $grey;\n$subtle-border-color: #DDD;\n$highlight-border-color: #777;\n$shadow-color: rgba(169, 173, 186, 0.2);\n$subtle-border: 1px dashed $subtle-border-color;\n$grey-border: $subtle-border-color; //factor this out for all customers\n$drop-zone-border-color: #9b9b9b; //default  #9b9b9b\n$drop-zone-border-hover: #4156C5; //default  #4156C5\n\n//padding\n$primary-padding: 3em;\n$secondary-padding: 2em;\n$tertiary-padding: 1em;\n$thin-padding: 0.3em;\n$full-width-thin-padding: calc(100% - 0.6em);\n$input-padding: 0.3em;\n\n$width-content-constrained: 1000px;\n\n$button-border-width: 1px;\n$button-border-strength: solid;\n$button-full-width: calc(100% - 2px);\n\n$input-full-width: calc(100% - 0.6em);\n\n//text sizes\n$base-font-size: 14px;\n\n$text-xx-large: 2.5em;\n$text-x-large: 2.0em;\n$text-large: 1.5em;\n$text-medium: 1.2em;\n$text-small: 0.9em;\n$text-x-small: 0.8em;\n\n//@media sizes\n$break-point-xx-large: 1400px;\n$break-point-x-large: 1290px;\n$break-point-large: 1024px;\n$break-point-tablet: 800px;\n$break-point-mobile: 500px;\n$break-point-phone: 300px;\n$break-point-phone: 300px;\n"
  },
  {
    "path": "client/scss/_video.scss",
    "content": "video:-moz-full-screen {\n  border:none;\n  padding:0;\n}\nvideo:-webkit-full-screen {\n  border:none;\n  padding:0;\n}\nvideo:fullscreen {\n  border:none;\n  padding:0;\n}\n"
  },
  {
    "path": "client/scss/all.scss",
    "content": "@import '~scss/_variables';\n@import '~scss/_reset';\n@import '~scss/font/_font.scss';\n@import '~scss/_html';\n@import '~scss/_body';\n@import '~scss/_react-app';\n@import '~scss/_text';\n@import '~scss/_markdown';\n\n@import '~scss/_link';\n@import '~scss/_input';\n@import '~scss/_select';\n@import '~scss/_textarea';\n@import '~scss/_video';\n@import '~scss/_form';\n\n@import '~scss/_asset-display';\n@import '~scss/_asset-preview';\n@import '~scss/_asset-blocked';\n@import '~scss/_button';\n@import '~scss/_button-primary';\n@import '~scss/_button-secondary';\n@import '~scss/_claim-pending';\n@import '~scss/_click-to-copy';\n@import '~scss/_form-feedback';\n@import '~scss/_horizontal-split';\n@import '~scss/_label';\n@import '~scss/_nav-bar';\n@import '~scss/_page-layout';\n@import '~scss/_page-layout-show-lite';\n@import '~scss/_page-content';\n@import '~scss/_progress-bar';\n@import '~scss/_publish-preview';\n@import '~scss/_share-buttons';\n@import '~scss/_space-between';\n@import '~scss/_space-around';\n@import '~scss/_row';\n@import '~scss/_tooltip';\n@import '~scss/_social-share-link';\n\n@import '~scss/_channel-claims-display';\n@import '~scss/_dropzone';\n@import '~scss/_publish-url-input';\n@import '~scss/_publish-status';\n@import '~scss/_publish-disabled-message';\n\n@import '~scss/_media-queries';\n"
  },
  {
    "path": "client/scss/font/Lekton/OFL.txt",
    "content": "Copyright (c) 2008-2010, Isia Urbino (http://www.isiaurbino.net)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "client/scss/font/_font.scss",
    "content": "@font-face {\n  font-family: 'Lekton';\n  src: url('./font/Lekton/Lekton-Regular.ttf');\n}\n\n@font-face {\n  font-family: 'Lekton';\n  src: url('./font/Lekton/Lekton-Bold.ttf');\n  font-weight: bold;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Lekton';\n  src: url('./font/Lekton/Lekton-Italic.ttf');\n  font-weight: normal;\n  font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Circular';\n  src: url('./font/Circular/CircularStd-Book.ttf');\n  font-weight: normal;\n}\n\n@font-face {\n  font-family: 'Circular';\n  src: url('./font/Circular/CircularStd-Bold.ttf');\n  font-weight: bold;\n}"
  },
  {
    "path": "client/src/actions/channel.js",
    "content": "import * as actions from '../constants/channel_action_types';\n\n// export action creators\n\nexport function updateLoggedInChannel (name, shortId, longId) {\n  return {\n    type: actions.CHANNEL_UPDATE,\n    data: {\n      name,\n      shortId,\n      longId,\n    },\n  };\n}\n\nexport function checkForLoggedInChannel () {\n  return {\n    type: actions.CHANNEL_LOGIN_CHECK,\n  };\n}\n\nexport function logOutChannel () {\n  return {\n    type: actions.CHANNEL_LOGOUT,\n  };\n}\n"
  },
  {
    "path": "client/src/actions/channelCreate.js",
    "content": "import * as actions from '../constants/channel_create_action_types';\n\n// export action creators\n\nexport function updateChannelCreateName (name, value) {\n  return {\n    type: actions.CHANNEL_CREATE_UPDATE_NAME,\n    data: {\n      name,\n      value,\n    },\n  };\n}\n\nexport function updateChannelCreatePassword (name, value) {\n  return {\n    type: actions.CHANNEL_CREATE_UPDATE_PASSWORD,\n    data: {\n      name,\n      value,\n    },\n  };\n}\n\nexport function updateChannelCreateStatus (status) {\n  return {\n    type: actions.CHANNEL_CREATE_UPDATE_STATUS,\n    data: status,\n  };\n}\n\nexport function updateChannelAvailability (channel) {\n  return {\n    type: actions.CHANNEL_AVAILABILITY,\n    data: channel,\n  };\n}\n\nexport function createChannel () {\n  return {\n    type: actions.CHANNEL_CREATE,\n  };\n}\n"
  },
  {
    "path": "client/src/actions/index.js",
    "content": "// import { } from './channel';\n// import { } from './publish';\nimport { onHandleShowPageUri } from './show';\n\nexport default {\n  onHandleShowPageUri,\n};\n"
  },
  {
    "path": "client/src/actions/publish.js",
    "content": "import * as actions from '../constants/publish_action_types';\n\n// export action creators\nexport function selectFile (file) {\n  return {\n    type: actions.FILE_SELECTED,\n    data: file,\n  };\n}\n\nexport function clearFile () {\n  return {\n    type: actions.FILE_CLEAR,\n  };\n}\n\nexport function setUpdateTrue () {\n  return {\n    type: actions.SET_UPDATE_TRUE,\n  };\n}\n\nexport function setHasChanged (status) {\n  return {\n    type: actions.SET_HAS_CHANGED,\n    data: status,\n  };\n}\n\nexport function updateMetadata (name, value) {\n  return {\n    type: actions.METADATA_UPDATE,\n    data: {\n      name,\n      value,\n    },\n  };\n}\n\nexport function updateClaim (value) {\n  return {\n    type: actions.CLAIM_UPDATE,\n    data: value,\n  };\n};\n\nexport function abandonClaim (data) {\n  return {\n    type: actions.ABANDON_CLAIM,\n    data,\n  };\n};\n\nexport function setPublishInChannel (channel) {\n  return {\n    type: actions.SET_PUBLISH_IN_CHANNEL,\n    channel,\n  };\n}\n\nexport function updatePublishStatus (status, message) {\n  return {\n    type: actions.PUBLISH_STATUS_UPDATE,\n    data: {\n      status,\n      message,\n    },\n  };\n}\n\nexport function updateError (name, value) {\n  return {\n    type: actions.ERROR_UPDATE,\n    data: {\n      name,\n      value,\n    },\n  };\n}\n\nexport function updateSelectedChannel (channelName) {\n  return {\n    type: actions.SELECTED_CHANNEL_UPDATE,\n    data: channelName,\n  };\n}\n\nexport function toggleMetadataInputs (showMetadataInputs) {\n  return {\n    type: actions.TOGGLE_METADATA_INPUTS,\n    data: showMetadataInputs,\n  };\n}\n\nexport function onNewThumbnail (file) {\n  return {\n    type: actions.THUMBNAIL_NEW,\n    data: file,\n  };\n}\n\nexport function startPublish (history) {\n  return {\n    type: actions.PUBLISH_START,\n    data: { history },\n  };\n}\n\nexport function validateClaim (claim) {\n  return {\n    type: actions.CLAIM_AVAILABILITY,\n    data: claim,\n  };\n}\n"
  },
  {
    "path": "client/src/actions/show.js",
    "content": "import * as actions from '../constants/show_action_types';\nimport { ASSET_DETAILS, ASSET_LITE, CHANNEL, SPECIAL_ASSET } from '../constants/show_request_types';\n\n// basic request parsing\nexport function onHandleShowPageUri(params, url) {\n  return {\n    type: actions.HANDLE_SHOW_URI,\n    data: {\n      ...params,\n      url,\n    },\n  };\n}\n\nexport function onHandleShowHomepage(params, url) {\n  return {\n    type: actions.HANDLE_SHOW_HOMEPAGE,\n    data: {\n      ...params,\n      url,\n    },\n  };\n}\n\nexport function onRequestError(error) {\n  return {\n    type: actions.REQUEST_ERROR,\n    data: error,\n  };\n}\n\nexport function onNewChannelRequest(channelName, channelId) {\n  const requestType = CHANNEL;\n  const requestId = `cr#${channelName}#${channelId}`;\n  return {\n    type: actions.CHANNEL_REQUEST_NEW,\n    data: { requestType, requestId, channelName, channelId },\n  };\n}\n\nexport function onNewSpecialAssetRequest(name) {\n  const requestType = SPECIAL_ASSET;\n  const requestId = `sar#${name}`;\n  return {\n    type: actions.SPECIAL_ASSET_REQUEST_NEW,\n    data: { requestType, requestId, name, channelName: name, channelId: name },\n  };\n}\n\nexport function onNewAssetRequest(name, id, channelName, channelId, extension) {\n  const requestType = extension ? ASSET_LITE : ASSET_DETAILS;\n  const requestId = `ar#${name}#${id}#${channelName}#${channelId}`;\n  return {\n    type: actions.ASSET_REQUEST_NEW,\n    data: {\n      requestType,\n      requestId,\n      name,\n      modifier: {\n        id,\n        channel: {\n          name: channelName,\n          id: channelId,\n        },\n      },\n    },\n  };\n}\n\nexport function onRequestUpdate(requestType, requestId) {\n  return {\n    type: actions.REQUEST_UPDATE,\n    data: {\n      requestType,\n      requestId,\n    },\n  };\n}\n\nexport function addRequestToRequestList(id, error, key) {\n  return {\n    type: actions.REQUEST_LIST_ADD,\n    data: { id, error, key },\n  };\n}\n\n// asset actions\n\nexport function addAssetToAssetList(id, error, name, claimId, shortId, claimData, claimViews) {\n  return {\n    type: actions.ASSET_ADD,\n    data: { id, error, name, claimId, shortId, claimData, claimViews },\n  };\n}\n\nexport function updateAssetViewsInList(id, claimId, claimViews) {\n  return {\n    type: actions.ASSET_VIEWS_UPDATE,\n    data: { id, claimId, claimViews },\n  };\n}\n\nexport function removeAsset(data) {\n  return {\n    type: actions.ASSET_REMOVE,\n    data,\n  };\n}\n\n// channel actions\n\nexport function addNewChannelToChannelList(id, name, shortId, longId, claimsData) {\n  return {\n    type: actions.CHANNEL_ADD,\n    data: {\n      id,\n      name,\n      shortId,\n      longId,\n      claimsData,\n    },\n  };\n}\n\nexport function onUpdateChannelClaims(channelKey, name, longId, page) {\n  return {\n    type: actions.CHANNEL_CLAIMS_UPDATE_ASYNC,\n    data: { channelKey, name, longId, page },\n  };\n}\n\nexport function updateChannelClaims(channelListId, claimsData) {\n  return {\n    type: actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED,\n    data: { channelListId, claimsData },\n  };\n}\n\n// display a file\n\nexport function fileRequested(name, claimId) {\n  return {\n    type: actions.FILE_REQUESTED,\n    data: { name, claimId },\n  };\n}\n\nexport function updateFileAvailability(status) {\n  return {\n    type: actions.FILE_AVAILABILITY_UPDATE,\n    data: status,\n  };\n}\n\nexport function updateDisplayAssetError(error) {\n  return {\n    type: actions.DISPLAY_ASSET_ERROR,\n    data: error,\n  };\n}\n\n// viewer settings\nexport function toggleDetailsExpanded(isExpanded) {\n  return {\n    type: actions.TOGGLE_DETAILS_EXPANDED,\n    data: isExpanded,\n  };\n}\n"
  },
  {
    "path": "client/src/api/assetApi.js",
    "content": "import Request from '../utils/request';\n\nexport function getLongClaimId(host, name, modifier) {\n  let body = {};\n  // create request params\n  if (modifier) {\n    if (modifier.id) {\n      body['claimId'] = modifier.id;\n    } else {\n      body['channelName'] = modifier.channel.name;\n      body['channelClaimId'] = modifier.channel.id;\n    }\n  }\n  body['claimName'] = name;\n  const params = {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(body),\n  };\n  // create url\n  const url = `${host}/api/claim/long-id`;\n  // return the request promise\n  return Request(url, params);\n}\n\nexport function getShortId(host, name, claimId) {\n  const url = `${host}/api/claim/short-id/${claimId}/${name}`;\n  return Request(url);\n}\n\nexport function getClaimData(host, name, claimId) {\n  const url = `${host}/api/claim/data/${name}/${claimId}`;\n  return Request(url);\n}\n\nexport function checkClaimAvailability(claim) {\n  const url = `/api/claim/availability/${claim}`;\n  return Request(url);\n}\n\nexport function getClaimViews(claimId) {\n  const url = `/api/claim/views/${claimId}`;\n  return Request(url);\n}\n\nexport function doAbandonClaim(outpoint) {\n  const params = {\n    method: 'POST',\n    body: JSON.stringify({ outpoint }),\n    headers: new Headers({\n      'Content-Type': 'application/json',\n    }),\n    credentials: 'include',\n  };\n  return Request('/api/claim/abandon', params);\n}\n"
  },
  {
    "path": "client/src/api/authApi.js",
    "content": "import Request from '../utils/request';\n\nexport function checkForLoggedInChannelApi () {\n  const url = `/user`;\n  const params = {credentials: 'include'};\n  return Request(url, params);\n}\n\nexport function channelLogoutApi () {\n  const url = `/logout`;\n  const params = {credentials: 'include'};\n  return Request(url, params);\n}\n"
  },
  {
    "path": "client/src/api/channelApi.js",
    "content": "import Request from '../utils/request';\n\nexport function getChannelData (host, name, id) {\n  if (!id) id = 'none';\n  const url = `${host}/api/channel/data/${name}/${id}`;\n  return Request(url);\n}\n\nexport function getChannelClaims (host, name, longId, page) {\n  if (!page) page = 1;\n  const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;\n  return Request(url);\n}\n\nexport function checkChannelAvailability (channel) {\n  const url = `/api/channel/availability/${channel}`;\n  return Request(url);\n}\n\nexport function makeCreateChannelRequest (username, password) {\n  const params = {\n    method : 'POST',\n    body   : JSON.stringify({username, password}),\n    headers: new Headers({\n      'Content-Type': 'application/json',\n    }),\n    credentials: 'include',\n  };\n  return Request('/signup', params);\n}\n"
  },
  {
    "path": "client/src/api/fileApi.js",
    "content": "import Request from '../utils/request';\n\nexport function checkFileAvailability (claimId, host, name) {\n  const url = `${host}/api/file/availability/${name}/${claimId}`;\n  return Request(url);\n}\n\nexport function triggerClaimGet (claimId, host, name) {\n  const url = `${host}/api/claim/get/${name}/${claimId}`;\n  return Request(url);\n}\n"
  },
  {
    "path": "client/src/api/homepageApi.js",
    "content": "import Request from '../utils/request';\n\nexport function getHomepageChannelsData (host, name, id) {\n  const url = `${host}/api/homepage/data/channels`;\n  return Request(url);\n}\n"
  },
  {
    "path": "client/src/api/specialAssetApi.js",
    "content": "import Request from '../utils/request';\n\nexport function getSpecialAssetClaims (host, name, page) {\n  if (!page) page = 1;\n  const url = `${host}/api/special/${name}/${page}`;\n  return Request(url);\n}\n"
  },
  {
    "path": "client/src/app.js",
    "content": "import React from 'react';\nimport { hot } from 'react-hot-loader/root'\nimport { Route, Switch } from 'react-router-dom';\n\nimport HomePage  from '@pages/HomePage';\nimport AboutPage from '@pages/AboutPage';\nimport TosPage from '@pages/TosPage';\nimport FaqPage from '@pages/FaqPage';\nimport LoginPage from '@pages/LoginPage';\nimport ContentPageWrapper from '@pages/ContentPageWrapper';\nimport FourOhFourPage from '@pages/FourOhFourPage';\nimport MultisitePage from '@pages/MultisitePage';\nimport PopularPage from '@pages/PopularPage';\nimport EditPage from '@pages/EditPage';\n\nconst App = () => {\n  return (\n    <Switch>\n      <Route exact path='/' component={AboutPage} />\n      <Route exact path='/about' component={AboutPage} />\n      <Route exact path='/tos' component={TosPage} />\n      <Route exact path='/faq' component={FaqPage} />\n      <Route exact path='/login' component={LoginPage} />\n      <Route exact path='/multisite' component={MultisitePage} />\n      <Route exact path='/popular' component={PopularPage} />\n      <Route exact path='/edit/:identifier/:claim' component={EditPage} />\n      <Route exact path='/:identifier/:claim' component={ContentPageWrapper} />\n      <Route exact path='/:claim' component={ContentPageWrapper} />\n      <Route component={FourOhFourPage} />\n    </Switch>\n  );\n};\n\nexport default hot(App);\n"
  },
  {
    "path": "client/src/channels/publish.js",
    "content": "import {buffers, END, eventChannel} from 'redux-saga';\n\nexport const makePublishRequestChannel = (fd, isUpdate) => {\n  return eventChannel(emitter => {\n    const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`;\n    const xhr = new XMLHttpRequest();\n    // add event listeners\n    const onLoadStart = () => {\n      emitter({loadStart: true});\n    };\n    const onProgress = (event) => {\n      if (event.lengthComputable) {\n        const percentage = Math.round((event.loaded * 100) / event.total);\n        emitter({progress: percentage});\n      }\n    };\n    const onLoad = () => {\n      emitter({load: true});\n    };\n    xhr.upload.addEventListener('loadstart', onLoadStart);\n    xhr.upload.addEventListener('progress', onProgress);\n    xhr.upload.addEventListener('load', onLoad);\n    // set state change handler\n    xhr.onreadystatechange = () => {\n      if (xhr.readyState === XMLHttpRequest.DONE) {\n        switch (xhr.status) {\n          case 413:\n            emitter({error: new Error(\"Unfortunately it appears this web server \" +\n              \"has been misconfigured, please inform the service administrators \" +\n              \"that they must set their nginx/apache request size maximums higher \" +\n              \"than their file size limits.\")});\n            emitter(END);\n            break;\n          case 200:\n            var response = JSON.parse(xhr.response);\n            if (response.success) {\n              emitter({success: response});\n              emitter(END);\n            } else {\n              emitter({error: new Error(response.message)});\n              emitter(END);\n            }\n            break;\n          default:\n            emitter({error: new Error(\"Received an unexpected response from \" +\n              \"server: \" + xhr.status)});\n            emitter(END);\n        }\n      }\n    };\n    // open and send\n    xhr.open('POST', uri, true);\n    xhr.send(fd);\n    // clean up\n    return () => {\n      xhr.upload.removeEventListener('loadstart', onLoadStart);\n      xhr.upload.removeEventListener('progress', onProgress);\n      xhr.upload.removeEventListener('load', onLoad);\n      xhr.onreadystatechange = null;\n      xhr.abort();\n    };\n  }, buffers.sliding(2));\n};\n"
  },
  {
    "path": "client/src/components/AboutSpeechDetails/index.jsx",
    "content": "import React from 'react';\nimport Row from '@components/Row';\n\nconst AboutSpeechDetails = () => {\n  return (\n    <div>\n      <Row>\n        <p className={'text--large'}>\n          Spee.ch's journey may be on hold, but LBRY is still on mission. We'd like to thank all of our testers and early adopters for helping us explore this use case.\n          We're really excited about <a className='link--primary' href='https://lbry.tv' target='_blank'>lbry.tv</a> and can't wait to see you over there for a fully featured experience.\n        </p>\n      </Row>\n    </div>\n  );\n};\n\nexport default AboutSpeechDetails;\n"
  },
  {
    "path": "client/src/components/AboutSpeechOverview/index.jsx",
    "content": "import React from 'react';\nimport Row from '@components/Row';\n\nconst AboutSpeechOverview = () => {\n  return (\n    <div>\n      <Row>\n        <p className={'text--extra-large'}>Lbry is no longer supporting Spee.ch. However, we're excited to show you <a className='link--primary' href='https://lbry.tv' target='_blank'>lbry.tv</a>!</p>\n      </Row>\n      <Row>\n        <div className={'text--large'}>\n          <a className='link--primary' target='_blank' href='https://twitter.com/lbry'>TWITTER</a><br/>\n          <a className='link--primary' target='_blank' href='https://github.com/lbryio/'>GITHUB</a><br/>\n          <a className='link--primary' target='_blank' href='https://discord.gg/YjYbwhS'>DISCORD CHANNEL</a><br/>\n        </div>\n      </Row>\n    </div>\n  );\n};\n\nexport default AboutSpeechOverview;\n"
  },
  {
    "path": "client/src/components/ActiveStatusBar/index.jsx",
    "content": "import React from 'react';\n\nconst ActiveStatusBar = () => {\n  return <span className='progress-bar--active'>| </span>;\n};\n\nexport default ActiveStatusBar;\n"
  },
  {
    "path": "client/src/components/AssetInfoFooter/index.js",
    "content": "import React from 'react';\nimport Row from '@components/Row';\n\nconst AssetInfoFooter = ({ assetUrl, name }) => {\n  return (\n    <div className=\"asset-footer\">\n      <p>\n        Hosted via the{' '}\n        <a className={'link--primary'} href={'https://lbry.com/get'} target={'_blank'}>\n          LBRY\n        </a>{' '}\n        blockchain\n      </p>\n    </div>\n  );\n};\n\nexport default AssetInfoFooter;\n"
  },
  {
    "path": "client/src/components/AssetPreview/index.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\nimport * as Icon from 'react-feather';\nimport Img from 'react-image';\n\nconst AssetPreview = ({ defaultThumbnail, claimData }) => {\n  const {name, fileExt, contentType, thumbnail, title, blocked, transactionTime = 0} = claimData;\n  const showUrl = createCanonicalLink({asset: {...claimData}});\n  const embedUrl = `${showUrl}.${fileExt}`;\n  const ago = Date.now() / 1000 - transactionTime;\n  const dayInSeconds = 60 * 60 * 24;\n  const monthInSeconds = dayInSeconds * 30;\n  let when;\n\n  if (ago < dayInSeconds || transactionTime < 1) {\n    when = 'Just today';\n  } else if (ago < monthInSeconds) {\n    when = `${Math.floor(ago / dayInSeconds)} d ago`;\n  } else {\n    when = `${Math.floor(ago / monthInSeconds)} mo ago`;\n  }\n  /*\n  we'll be assigning media icon based on supported type / mime types\n  */\n  const media = contentType.split('/')[0];\n  /*\n  make sure thumb has the right url\n   */\n  const thumb = media === 'image' ? embedUrl : thumbnail;\n  /*\n  This blocked section shouldn't be necessary after pagination is reworked,\n  though it might be useful for channel_mine situations.\n  */\n\n  if (blocked) {\n    return (\n      <div className='asset-preview'>\n        <div className='asset-preview__blocked'>\n          <p>Error 451</p>\n          <p>This content is blocked for legal reasons.</p>\n        </div>\n        <div className={'asset-preview__label'}>\n          <div className={'asset-preview__label-text'}>\n            <p className='asset-preview__title text--medium'>Blocked Content</p>\n          </div>\n        </div>\n      </div>\n    );\n  } else {\n    return (\n      <Link to={showUrl} className='asset-preview'>\n        <div className='asset-preview__image-box'>\n          <Img\n            src={[\n              thumb,\n              defaultThumbnail,\n              '/assets/img/default_thumb.jpg',\n            ]}\n            alt={name}\n            className={'asset-preview__image'}\n          />\n        </div>\n\n        <div className={'asset-preview__label'}>\n\n          <div className={'asset-preview__label-text'}>\n            <p>{title}</p>\n          </div>\n          <div className={'asset-preview__label-info '}>\n            <div className={'asset-preview__label-info-datum'}>\n              <div className={'svg-icon'}>\n                { media === 'image' && <Icon.Image />}\n                { media === 'text' && <Icon.FileText />}\n                { media === 'video' && contentType === 'video/mp4' && <Icon.Video />}\n                { media !== 'image' && media !== 'text' && contentType !== 'video/mp4' && <Icon.File />}\n              </div>\n              <div>{fileExt}</div>\n            </div>\n\n            <div className={'asset-preview__label-info-datum'}>\n              <div>{when}</div>\n            </div>\n          </div>\n        </div>\n      </Link>\n    );\n  }\n};\n\nexport default AssetPreview;\n"
  },
  {
    "path": "client/src/components/AssetShareButtons/index.js",
    "content": "import React from 'react';\nimport SocialShareLink from '@components/SocialShareLink';\n\nconst AssetShareButtons = ({ assetUrl, name }) => {\n  return (\n    <SocialShareLink >\n      <a\n        className='link--primary twitter'\n        target='_blank'\n        href={`https://twitter.com/intent/tweet?text=${assetUrl}`}\n      >\n        <img src='/assets/img/icn_twitter.svg' />\n      </a>\n      <a\n        className='link--primary facebook'\n        target='_blank'\n        href={`https://www.facebook.com/sharer/sharer.php?u=${assetUrl}`}\n      >\n        <img src='/assets/img/icn_facebook.svg' />\n      </a>\n      <a\n        className='link--primary tumblr'\n        target='_blank'\n        href={`https://tumblr.com/widgets/share/tool?canonicalUrl=${assetUrl}`}\n      >\n        <img src='/assets/img/icn_tumblr.svg' />\n      </a>\n      <a\n        className='link--primary reddit'\n        target='_blank'\n        href={`https://www.reddit.com/submit?url=${assetUrl}&title=${name}`}\n      >\n        <img src='/assets/img/icn_reddit.svg' />\n      </a>\n    </SocialShareLink>\n  );\n};\n//\n// Additional icons disabled. If you want to add additional icons, you have to solve\n// https://github.com/lbryio/spee.ch/issues/687\n//\n// <a\n//   className='link--primary'\n//   target='_blank'\n//   href={`https://sharetomastodon.github.io/?title=${name}&url=${assetUrl}`}\n// >\n//   mastodon\n// </a>\n// <a\n//   className='link--primary'\n//   target='_blank'\n//   href={`https://share.diasporafoundation.org/?title=${name}&url=${assetUrl}`}\n// >\n//   diaspora\n// </a>\n\nexport default AssetShareButtons;\n"
  },
  {
    "path": "client/src/components/ButtonPrimary/index.jsx",
    "content": "import React from 'react';\n\nconst ButtonPrimary  = ({ value, onClickHandler, type = 'button' }) => {\n  return (\n    <button\n      type={type}\n      className={'button button--primary'}\n      onClick={onClickHandler}\n    >\n      {value}\n    </button>\n  );\n};\n\nexport default ButtonPrimary;\n"
  },
  {
    "path": "client/src/components/ButtonPrimaryJumbo/index.jsx",
    "content": "import React from 'react';\n\nconst ButtonPrimaryJumbo  = ({ value, onClickHandler }) => {\n  return (\n    <button\n      className={'button button--primary button--jumbo'}\n      onClick={onClickHandler}\n    >\n      {value}\n    </button>\n  );\n};\n\nexport default ButtonPrimaryJumbo;\n"
  },
  {
    "path": "client/src/components/ButtonSecondary/index.jsx",
    "content": "import React from 'react';\n\nconst ButtonPrimary  = ({ value, onClickHandler }) => {\n  return (\n    <button\n      className={'button button--secondary'}\n      onClick={onClickHandler}\n    >\n      {value}\n    </button>\n  );\n};\n\nexport default ButtonPrimary;\n"
  },
  {
    "path": "client/src/components/ChannelAbout/index.jsx",
    "content": "import React from 'react';\n\nconst ChannelAbout = () => {\n  return (\n    <div className={'text--large'}>\n      <p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends.</p>\n      <p>You can create 1 channel, or 100, so whether you're <a className='link--primary' target='_blank' href='/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8'>documenting important events</a>, or making a public repository for <a className='link--primary' target='_blank' href='/@catGifs'>cat gifs</a> (password: '1234'), try creating a channel for it!</p>\n    </div>\n  );\n};\n\nexport default ChannelAbout;\n"
  },
  {
    "path": "client/src/components/ChannelCreateNameInput/index.jsx",
    "content": "import React from 'react';\nimport Label from '@components/Label';\nimport RowLabeled from '@components/RowLabeled';\n\nconst ChannelCreateNameInput  = ({ value, error, handleNameInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Name:'} />\n      }\n      content={\n        <div className='input-area'>\n          <span>@</span>\n          <input\n            type='text'\n            name='channel'\n            className='input-text'\n            placeholder='exampleChannelName'\n            value={value}\n            onChange={handleNameInput}\n          />\n          { (value && !error) && (\n            <span className='info-message--success span--absolute'>\n              {'\\u2713'}\n            </span>\n          )}\n          { error && (\n            <span className='info-message--failure span--absolute'>\n              {'\\u2716'}\n            </span>\n          )}\n        </div>\n      }\n    />\n  );\n};\n\nexport default ChannelCreateNameInput;\n"
  },
  {
    "path": "client/src/components/ChannelCreatePasswordInput/index.jsx",
    "content": "import React from 'react';\nimport Label from '@components/Label';\nimport RowLabeled from '@components/RowLabeled';\n\nconst ChannelCreatePasswordInput  = ({ value, handlePasswordInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Password:'} />\n      }\n      content={\n        <div className='input-area'>\n          <input\n            type='password'\n            name='password'\n            className='input-text'\n            placeholder=''\n            value={value}\n            onChange={handlePasswordInput} />\n        </div>\n      }\n    />\n  );\n};\n\nexport default ChannelCreatePasswordInput;\n"
  },
  {
    "path": "client/src/components/ChannelInfoDisplay/index.jsx",
    "content": "import React from 'react';\n// TODO: factor out longId OR implement tooltip display\nconst ChannelInfoDisplay = ({name, longId, shortId}) => {\n  return (\n    <div>\n      <h2>{name}:{shortId}</h2>\n    </div>\n  );\n};\n\nexport default ChannelInfoDisplay;\n"
  },
  {
    "path": "client/src/components/ChannelLoginNameInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\n\nconst ChannelLoginNameInput  = ({ channelName, handleInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Name:'} />\n      }\n      content={\n        <div className='input-area'>\n          <span>@</span>\n          <input\n            type='text'\n            id='channel-login-name-input'\n            className='input-text'\n            name='name'\n            placeholder='Your Channel Name'\n            value={channelName}\n            onChange={handleInput}\n          />\n        </div>\n      }\n    />\n  );\n};\n\nexport default ChannelLoginNameInput;\n"
  },
  {
    "path": "client/src/components/ChannelLoginPasswordInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\n\nconst ChannelLoginPasswordInput  = ({ channelPassword, handleInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Password:'} />\n      }\n      content={\n        <div className='input-area'>\n          <input\n            type='password'\n            id='channel-login-password-input'\n            name='password'\n            className='input-text'\n            placeholder=''\n            value={channelPassword}\n            onChange={handleInput}\n          />\n        </div>\n      }\n    />\n  );\n};\n\nexport default ChannelLoginPasswordInput;\n"
  },
  {
    "path": "client/src/components/ChannelSelectDropdown/index.jsx",
    "content": "import React from 'react';\nimport { LOGIN, CREATE } from '../../constants/publish_channel_select_states';\n\nconst ChannelSelectDropdown = ({ selectedChannel, handleSelection, loggedInChannelName }) => {\n  return (\n    <select\n      id='channel-name-select'\n      value={selectedChannel}\n      onChange={handleSelection}>\n      { loggedInChannelName && (\n        <option value={loggedInChannelName} >{loggedInChannelName}</option>\n      )}\n      <option value={LOGIN}>Existing</option>\n      <option value={CREATE}>New</option>\n    </select>\n  );\n};\n\nexport default ChannelSelectDropdown;\n"
  },
  {
    "path": "client/src/components/ChooseAnonymousPublishRadio/index.jsx",
    "content": "import React from 'react';\n\nconst ChooseAnonymousPublishRadio = ({ publishInChannel, toggleAnonymousPublish }) => {\n  return (\n    <div>\n      <input\n        type='radio'\n        name='anonymous-or-channel'\n        id='anonymous-radio'\n        className='input-radio'\n        value='anonymous'\n        checked={!publishInChannel}\n        onChange={toggleAnonymousPublish}\n      />\n      <label\n        className='label-radio'\n        htmlFor='anonymous-radio'\n      >\n        Anonymous\n      </label>\n    </div>\n  );\n};\n\nexport default ChooseAnonymousPublishRadio;\n"
  },
  {
    "path": "client/src/components/ChooseChannelPublishRadio/index.jsx",
    "content": "import React from 'react';\n\nconst ChooseChannelPublishRadio = ({ publishInChannel, toggleAnonymousPublish }) => {\n  return (\n    <div>\n      <input\n        type='radio'\n        name='anonymous-or-channel'\n        id='channel-radio'\n        className='input-radio'\n        value='in a channel'\n        checked={publishInChannel}\n        onChange={toggleAnonymousPublish}\n      />\n      <label\n        className='label-radio'\n        htmlFor='channel-radio'\n      >\n        In a channel\n      </label>\n    </div>\n  );\n};\n\nexport default ChooseChannelPublishRadio;\n"
  },
  {
    "path": "client/src/components/ClickToCopy/index.jsx",
    "content": "import React from 'react';\nimport * as Icon from 'react-feather';\n\nclass ClickToCopy extends React.Component {\n  constructor (props) {\n    super(props);\n    this.copyToClipboard = this.copyToClipboard.bind(this);\n  }\n  copyToClipboard () {\n    const elementToCopy = this.props.id;\n    const element = document.getElementById(elementToCopy);\n    console.log(elementToCopy);\n    element.select();\n    try {\n      document.execCommand('copy');\n    } catch (err) {\n      this.setState({error: 'Oops, unable to copy'});\n    }\n  }\n  render () {\n    const {id, value} = this.props;\n    return (\n      <div\n        className='click-to-copy-wrap'\n        onClick={this.copyToClipboard}\n      >\n        <input\n          id={id}\n          value={value}\n          type='text'\n          className='click-to-copy'\n          readOnly\n          spellCheck='false'\n        />\n        <div className='icon-wrap'>\n          <Icon.Copy />\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default ClickToCopy;\n"
  },
  {
    "path": "client/src/components/DropzoneDropItDisplay/index.jsx",
    "content": "import React from 'react';\n\nconst DropzoneDropItDisplay = () => {\n  return (\n    <div className={'dropzone-dropit-display'}>\n      Drop it.\n    </div>\n  );\n};\n\nexport default DropzoneDropItDisplay;\n"
  },
  {
    "path": "client/src/components/DropzoneInstructionsDisplay/index.jsx",
    "content": "import React from 'react';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\nimport Row from '@components/Row';\n\nconst DropzoneInstructionsDisplay = ({fileError, message}) => {\n  if (!message) {\n    message = 'Drag & drop image or video here to publish';\n  }\n  return (\n    <div className={'dropzone-instructions-display'}>\n      <Row>\n        <span className={'text--large'}>{message}</span>\n      </Row>\n      <Row>\n        <span className={'text--small text--secondary'}>OR</span>\n      </Row>\n      { fileError ? (\n        <div>\n          <Row>\n            <span className={'text--large dropzone-instructions-display__chooser-label'}>CHOOSE FILE</span>\n          </Row>\n          <FormFeedbackDisplay\n            errorMessage={fileError}\n            defaultMessage={false}\n          />\n        </div>\n      ) : (\n        <span className={'text--large dropzone-instructions-display__chooser-label'}>CHOOSE FILE</span>\n      )}\n    </div>\n  );\n};\n\nexport default DropzoneInstructionsDisplay;\n"
  },
  {
    "path": "client/src/components/DropzonePreviewImage/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nclass PublishPreview extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      imgSource            : '',\n      defaultVideoThumbnail: '/assets/img/video_thumb_default.png',\n      defaultThumbnail     : '/assets/img/Speech_Logo_Main@OG-02.jpg',\n    };\n  }\n  componentDidMount () {\n    const { isUpdate, sourceUrl, file } = this.props;\n    if (isUpdate && sourceUrl) {\n      this.setState({ imgSource: sourceUrl });\n    } else {\n      this.setPreviewImageSource(file);\n    }\n  }\n  componentWillReceiveProps (newProps) {\n    if (newProps.file !== this.props.file) {\n      this.setPreviewImageSource(newProps.file);\n    }\n    if (newProps.thumbnail !== this.props.thumbnail) {\n      if (newProps.thumbnail) {\n        this.setPreviewImageSourceFromFile(newProps.thumbnail);\n      } else {\n        this.setState({imgSource: this.state.defaultThumbnail});\n      }\n    }\n  }\n  setPreviewImageSourceFromFile (file) {\n    const previewReader = new FileReader();\n    previewReader.readAsDataURL(file);\n    previewReader.onloadend = () => {\n      this.setState({imgSource: previewReader.result});\n    };\n  }\n  setPreviewImageSource (file) {\n    if (this.props.thumbnail) {\n      this.setPreviewImageSourceFromFile(this.props.thumbnail);\n    } else if (file.type.substr(0, file.type.indexOf('/')) === 'image'){\n      this.setPreviewImageSourceFromFile(file);\n    } else if (file.type === 'video'){\n      this.setState({imgSource: this.state.defaultVideoThumbnail});\n    } else {\n      this.setState({imgSource: this.state.defaultThumbnail});\n    }\n  }\n  render () {\n    return (\n      <img\n        src={this.state.imgSource}\n        className={'dropzone-preview-image ' + (this.props.dimPreview ? 'publish-preview-dim' : '')}\n        alt='publish preview'\n      />\n    );\n  }\n};\n\nPublishPreview.propTypes = {\n  dimPreview: PropTypes.bool.isRequired,\n  file      : PropTypes.object,\n  thumbnail : PropTypes.object,\n  isUpdate  : PropTypes.bool,\n  sourceUrl : PropTypes.string,\n};\n\nexport default PublishPreview;\n"
  },
  {
    "path": "client/src/components/ErrorBoundary/index.jsx",
    "content": "import React from 'react';\nclass ErrorBoundary extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  componentDidCatch(error, info) {\n    // Display fallback UI\n    this.setState({ hasError: true });\n    // You can also log the error to an error reporting service\n    console.log('Error occurred while rendering markdown')\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // You can render any custom fallback UI\n      return (<p>A component was prevented from crashing the App.</p>);\n    }\n    return this.props.children;\n  }\n}\n\nexport default ErrorBoundary;\n"
  },
  {
    "path": "client/src/components/ExpandingTextArea/index.jsx",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\n\nclass ExpandingTextarea extends Component {\n  constructor (props) {\n    super(props);\n    this._handleChange = this._handleChange.bind(this);\n  }\n  componentDidMount () {\n    this.adjustTextarea({});\n  }\n  _handleChange (event) {\n    const { onChange } = this.props;\n    if (onChange) onChange(event);\n    this.adjustTextarea(event);\n  }\n  adjustTextarea ({ target = this.el }) {\n    target.style.height = 0;\n    target.style.height = `${target.scrollHeight}px`;\n  }\n  render () {\n    const { ...rest } = this.props;\n    return (\n      <textarea\n        {...rest}\n        ref={x => this.el = x}\n        onChange={this._handleChange}\n      />\n    );\n  }\n}\n\nExpandingTextarea.propTypes = {\n  onChange: PropTypes.func,\n};\n\nexport default ExpandingTextarea;\n"
  },
  {
    "path": "client/src/components/FileViewer/index.jsx",
    "content": "import React from 'react';\nimport ReactMarkdown from 'react-markdown/with-html';\nimport { serving } from '@config/siteConfig.json';\nimport ErrorBoundary from '@components/ErrorBoundary';\nconst { markdownSettings: { escapeHtmlMain, skipHtmlMain, allowedTypesMain } } = serving;\nclass FileViewer extends React.Component {\n\n  constructor (props) {\n    super(props);\n    /*\n      Prevent memory leak by closing fetch before unmount\n     */\n    this.abortController = new AbortController();\n    this.abortSignal = this.abortController.signal;\n    this.state = {\n      fileLoaded: false,\n      fileText  : '',\n    };\n  }\n\n  componentDidMount () {\n    const {sourceUrl} = this.props;\n    const signal = this.abortSignal;\n    fetch(sourceUrl, { signal })\n      .then(response => response.text())\n      .then((text) => {\n        this.setState({fileText: text});\n        this.setState({fileLoaded: true});\n        return true;\n      })\n      .catch(e => { console.log('fetch aborted on unmount ', e) });\n  }\n\n  componentWillUnmount () {\n    this.abortController.abort();\n  }\n\n  render () {\n    return (\n      <div className={'markdown'}>\n        {\n          this.state.fileLoaded &&\n            <ErrorBoundary>\n              <ReactMarkdown className={'markdown-preview'} source={this.state.fileText} skipHtml={skipHtmlMain} allowedTypes={allowedTypesMain} escapeHtml={escapeHtmlMain} />\n            </ErrorBoundary>\n        }\n        {\n          !this.state.fileLoaded &&\n          <p>Loading your file...</p>\n        }\n      </div>\n    );\n  }\n}\n\nexport default FileViewer;\n"
  },
  {
    "path": "client/src/components/FormFeedbackDisplay/index.jsx",
    "content": "import React from 'react';\n\nconst FormFeedbackDisplay = ({ errorMessage, defaultMessage }) => {\n  return (errorMessage || defaultMessage) ? (\n    <div className={'form-feedback'}>\n      { errorMessage ? (\n        <span className={'text--small text--failure'}>{errorMessage}</span>\n      ) : (\n        <div>\n          { defaultMessage ? (\n            <span className={'text--small text--secondary'}>{defaultMessage}</span>\n          ) : (\n            <span className={'text--small'}>&nbsp;</span>\n          )}\n        </div>\n      )}\n    </div>\n  ) : null;\n};\n\nexport default FormFeedbackDisplay;\n"
  },
  {
    "path": "client/src/components/GAListener/index.jsx",
    "content": "import React from 'react';\nimport GoogleAnalytics from 'react-ga';\nimport { withRouter } from 'react-router-dom';\n\nimport siteConfig from '@config/siteConfig.json';\n\nlet googleId = null;\n\nif (siteConfig && siteConfig.analytics) {\n  ({ googleId } = siteConfig.analytics);\n}\n\nif (googleId) {\n  GoogleAnalytics.initialize(googleId);\n}\n\nclass GAListener extends React.Component {\n  componentDidMount () {\n    this.sendPageView(this.props.history.location);\n    this.props.history.listen(this.sendPageView);\n  }\n\n  sendPageView (location) {\n    if (googleId) {\n      GoogleAnalytics.set({ page: location.pathname });\n      GoogleAnalytics.pageview(location.pathname);\n    }\n  }\n\n  render () {\n    return this.props.children;\n  }\n}\n\nexport default withRouter(GAListener);\n"
  },
  {
    "path": "client/src/components/HorizontalSplit/index.jsx",
    "content": "import React from 'react';\n\nclass HorizontalSplit extends React.Component {\n  render () {\n    const { leftSide, rightSide, collapseOnMobile } = this.props;\n\n    let className = 'horizontal-split';\n    if (collapseOnMobile) {\n      className += \" horizontal-split--mobile-collapse\";\n    }\n\n    // If there is no left side, move the right side to the left\n    // This is mostly for content with no description\n    // It doesn't need to be on the right side with nothing next to it.\n    const leftComponent = leftSide || rightSide;\n    const rightComponent = leftSide ? rightSide : null;\n\n    return (\n      <div className={className}>\n        <div className={'horizontal-split__column horizontal-split__column--left'}>\n          {leftComponent}\n        </div>\n        <div className={'horizontal-split__column horizontal-split__column--right'}>\n          {rightComponent}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default HorizontalSplit;\n"
  },
  {
    "path": "client/src/components/InactiveStatusBar/index.jsx",
    "content": "import React from 'react';\n\nconst InactiveStatusBar = () => {\n  return <span className='progress-bar--inactive'>| </span>;\n};\n\nexport default InactiveStatusBar;\n"
  },
  {
    "path": "client/src/components/Label/index.jsx",
    "content": "import React from 'react';\n\nconst Label = ({ value }) => {\n  return (\n    <label\n      className='label'\n    >\n      {value}\n    </label>\n  );\n};\nexport default Label;\n"
  },
  {
    "path": "client/src/components/Logo/index.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\n\nfunction Logo () {\n  return (\n    <svg version='1.1' id='Layer_1' x='0px' y='0px' height='24px' viewBox='0 0 80 31' enableBackground='new 0 0 80 31' className='nav-bar-logo'>\n      <Link to='/'>\n        <title>Logo</title>\n        <desc>Spee.ch logo</desc>\n        <g id='About'>\n          <g id='Publish-Form-V2-_x28_filled_x29_' transform='translate(-42.000000, -23.000000)'>\n            <g id='Group-17' transform='translate(42.000000, 22.000000)'>\n              <text transform='matrix(1 0 0 1 0 20)' fontSize='25' fontFamily='Roboto'>Spee&lt;h</text>\n              <g id='Group-16' transform='translate(0.000000, 30.000000)'>\n                <path id='Line-8' fill='none' stroke='#09F911' strokeWidth='1' strokeLinecap='square' d='M0.5,1.5h15' />\n                <path id='Line-8-Copy' fill='none' stroke='#029D74' strokeWidth='1' strokeLinecap='square' d='M16.5,1.5h15' />\n                <path id='Line-8-Copy-2' fill='none' stroke='#E35BD8' strokeWidth='1' strokeLinecap='square' d='M32.5,1.5h15' />\n                <path id='Line-8-Copy-3' fill='none' stroke='#4156C5' strokeWidth='1' strokeLinecap='square' d='M48.5,1.5h15' />\n                <path id='Line-8-Copy-4' fill='none' stroke='#635688' strokeWidth='1' strokeLinecap='square' d='M64.5,1.5h15' />\n              </g>\n            </g>\n          </g>\n        </g>\n      </Link>\n    </svg>\n  );\n};\n\nexport default Logo;\n"
  },
  {
    "path": "client/src/components/Memeify/EditableFontface/index.js",
    "content": "import React, { Component } from 'react';\n\nconst DEFAULT_TEXT_RENDER = (text) => text;\n\nexport default class EditableFontface extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      blinkSelection: props.blinkSelection == false ? false : true,\n      value: props.value,\n    };\n\n    this.textInput = React.createRef();\n  }\n\n  componentDidMount() {\n    const textInput = this.textInput.current;\n\n    if(textInput) {\n      textInput.focus();\n    }\n  }\n\n  render() {\n    const me = this;\n\n    const {\n      blinkSelection,\n      value\n    } = me.state;\n\n    const {\n      editable = true,\n      fontFace,\n      preview,\n    } = me.props;\n\n    const textRender = fontFace.textRender || DEFAULT_TEXT_RENDER;\n\n    const textStyles = Object.assign({\n      ...(blinkSelection ? {\n        animation: 'textBlink 1s infinite',\n      } : {}),\n      minHeight: '20px',\n      WebkitFontSmoothing: 'antialiased',\n      MozOsxFontSmoothing: 'grayscale',\n    }, fontFace.text, preview ? fontFace.previewOverrides : {});\n\n    const fontInput = (editable === true) ? (\n      <input ref={this.textInput} type=\"text\" onKeyPress={(e) => me.onKeyPress(e)} onChange={(e) => me.onChange(e)} style={{\n        ...{\n          bottom: 0,\n          opacity: 0,\n          padding: 0,\n          left: 0,\n          position: 'absolute',\n          top: 0,\n          width: '100%',\n          zIndex: 1,\n        },\n        ...(fontFace.editorStyle || {}),\n      }} />\n    ) : null;\n\n    return (\n      <div style={{ position: 'relative', ...(fontFace.container || {}) }}>\n        <style scoped>{'@keyframes textBlink { 0% { opacity: 1 } 30% { opacity: 0.6 } 60% { opacity: 1 } }'}</style>\n        {fontInput}\n        <div ref={me.state.fontRender} style={textStyles} title={value}>{textRender(value)}</div>\n      </div>\n    );\n  }\n\n  onKeyPress(e) {\n    this.setState({\n      blinkSelection: false,\n      value: e.target.value\n    });\n  }\n\n  onChange(e) {\n    this.setState({\n      blinkSelection: false,\n      value: e.target.value\n    });\n  }\n};\n\nexport const PRESETS = {\n  'Green Machine': require('../FontFaces/GreenMachine').default,\n  'Inferno': require('../FontFaces/Inferno').default,\n  'Lazer': require('../FontFaces/Lazer').default,\n  'Neon': require('../FontFaces/Neon').default,\n  'Old Blue': require('../FontFaces/OldBlue').default,\n  'Outline': require('../FontFaces/Outline').default,\n  'Retro Rainbow': require('../FontFaces/RetroRainbow').default,\n  'The Special': require('../FontFaces/TheSpecial').default,\n  'Vapor Wave': require('../FontFaces/VaporWave').default,\n}\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/.gitkeep",
    "content": ""
  },
  {
    "path": "client/src/components/Memeify/FontFaces/GreenMachine.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'courier, Courier New',\n    fontWeight: 'bold',\n    fontSize: '2em',\n  },\n  text: {\n    color: '#00b700',\n    fontFamily: 'courier, Courier New',\n    fontSize: '2rem',\n    fontWeight: 'bold',\n    textShadow: '1px 1px 2px #003605',\n  },\n  previewOverrides: {\n    fontSize: '1.6em',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/Inferno.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'helvetica, Helvetica Nue',\n    fontWeight: 'bold',\n    fontSize: '2em',\n  },\n  text: {\n    fontFamily: 'helvetica, Helvetica Nue',\n    fontWeight: 'bold',\n    fontSize: '2em',\n    color: '#fff',\n    textShadow: '0px 0px 3px #c20000, 0px 0px 3px #e0f2ff, 0px 0px 5px #532761, 0px 0px 20px #670606, 0 0 10px rgba(0, 0, 0, .8), 0 0 10px #fefcc9, 5px -5px 15px #feec85, -10px -10px 20px #ffae34, 10px -20px 25px #ec760c, -10px -30px 30px #cd4606, 0 -40px 35px #973716, 5px -45px 40px #451b0e, 0 -2px 15px rgba(255, 200, 160, .5)',\n  },\n  previewOverrides: {\n    fontSize: '1.5em',\n    overflow: 'hidden',\n    padding: '0 1rem 0 1rem',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/Lazer.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'helvetica, Helvetica Nue',\n    fontWeight: 'bold',\n    fontSize: '2em',\n    textTransform: 'uppercase',\n    whiteSpace: 'nowrap',\n  },\n  text: {\n    fontFamily: 'helvetica, Helvetica Nue',\n    fontWeight: 'bold',\n    backgroundImage: 'linear-gradient(180deg, #249bff 0%, #e1f8ff 44%, #3a006b 44%, #ff57d6 100%)',\n    backgroundClip: 'text',\n    fontSize: '2em',\n    color: 'transparent',\n    filter: 'drop-shadow(0 0 .05rem black)',\n    textTransform: 'uppercase',\n    whiteSpace: 'nowrap',\n    WebkitBackgroundClip: 'text',\n    WebkitTextStroke: '0.03em rgba(255, 255, 255, 0.6)',\n  },\n  previewOverrides: {\n    fontSize: '1.8em',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/Neon.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'Helvetica, Arial',\n    fontWeight: 'bold',\n    fontSize: '2em',\n    letterSpacing: '.1em',\n  },\n  text: {\n    color: '#fff',\n    fontFamily: 'Helvetica, Arial',\n    fontSize: '2rem',\n    fontWeight: 'bold',\n    letterSpacing: '.1em',\n    textShadow: '0 0 0.05em #fff, 0 0 0.1em #fff, 0 0 0.2em #fff, 0 0 .2em #ff1de2, 0 0 .4em #ff26e3, 0 0 .5em #ff00de, 0 0 1em #ff61eb, 0 0 1.5em #ff7cee',\n  },\n  previewOverrides: {\n    fontSize: '1.8em',\n    padding: '0 1rem 0 1rem',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/OldBlue.js",
    "content": "import React from 'react';\n\nconst charToFullWidth = char => {\n\tconst c = char.charCodeAt( 0 )\n\treturn c >= 33 && c <= 126\n\t\t? String.fromCharCode( ( c - 33 ) + 65281 )\n\t\t: char\n}\n\nexport default {\n  container: {},\n  editorStyle: {},\n  text: {\n    fontFamily: 'Segoe UI,Helvetica,Arial',\n  },\n\tpreviewOverrides: {\n\t\theight: '2.6rem',\n\t\toverflow: 'hidden',\n\t},\n  textRender: (text) => {\n    const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`\n    return (\n      <svg viewBox=\"0 0 500 50\" style={{ height: '4em', fontFamily: 'Arial', fontWeight: 'bold' }}>\n\t\t\t\t<path id={id} fill=\"transparent\" d=\"M 0 50 Q 50 0 100 50 Q 150 100 200 50 Q 250 0 300 50 Q 350 100 400 50 Q 450 0 500 50 Q 550 100 600 50 \" transform=\"scale(1 0.5) translate(0 15)\" />\n        <text x=\"10\" style={{ fill: '#4dc2fe', fontWeight: 900, letterSpacing: '-0.15em', textShadow: '0.15em -0.1em #1c55a0' }}>\n          <textPath xlinkHref={`#${id}`}>\n            {text}\n          </textPath>\n        </text>\n\t\t\t\t<text x=\"10\" style={{ fill: 'transparent', stroke: '#1c55a0', strokeWidth: '.012em', fontWeight: 900, letterSpacing: '-0.15em' }}>\n          <textPath xlinkHref={`#${id}`}>\n            {text}\n          </textPath>\n        </text>\n      </svg>\n    );\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/Outline.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'arial',\n    fontWeight: 'bold',\n    fontSize: '2em',\n  },\n  text: {\n    color: '#fff',\n    fontFamily: 'arial',\n    fontSize: '2rem',\n    fontWeight: 'bold',\n    textShadow: '2px 2px .1px #000, -1px -1px .1px #000, 1px -1px .1px #000, -1px 1px .1px #000, 1px 1px .1px #000',\n  },\n  previewOverrides: {\n    fontSize: '1.6rem',\n    padding: '0 .04rem',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/RetroRainbow.js",
    "content": "export default {\n  editorStyle: {\n    fontFamily: 'Arial, sans-serif',\n    fontWeight: 'bold',\n    fontSize: '1.2em',\n    transform: 'scale(1, 1.5)',\n  },\n  text: {\n    fontFamily: 'Arial, sans-serif',\n    fontWeight: 'bold',\n    backgroundImage: 'linear-gradient(to right, #b306a9, #ef2667, #f42e2c, #ffa509, #fdfc00, #55ac2f, #0b13fd, #a804af)',\n    backgroundClip: 'text',\n    fontSize: '1.2em',\n    transform: 'scale(1, 1.5)',\n    color: 'transparent',\n    paddingBottom: '.25em',\n    paddingTop: '.1em',\n    WebkitBackgroundClip: 'text',\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/TheSpecial.js",
    "content": "import React from 'react';\n\nexport default {\n  editorStyle: {\n    fontFamily: 'Arial, sans-serif',\n    fontWeight: 'bold',\n    fontSize: '1.4em',\n  },\n  text: {\n    fontFamily: 'Arial, sans-serif',\n    fontWeight: 'bold',\n    backgroundImage: 'linear-gradient(to right, #b306a9, #ef2667, #f42e2c, #ffa509, #fdfc00, #55ac2f, #0b13fd, #a804af)',\n    backgroundClip: 'text',\n    fontSize: '1.4em',\n    color: 'transparent',\n    paddingBottom: '.25em',\n    paddingTop: '.1em',\n    WebkitBackgroundClip: 'text',\n  },\n  textRender: (text) => {\n    text = text\n      .replace(/love [^\\s.!$]+/g, 'love LBRY')\n      .replace(/LBRY/g, 'amazing LBRY')\n      .replace(/julie/gi, 'super Julie')\n      .replace(/tom/gi, 'amazing Tom')\n      .replace(/(btc|bch)/gi, 'LBC')\n      .replace(/\\w+ is \\w+/gi, 'LBRY is amazing');\n\n    return text.split(/chris[\\d\\w]*/gi).reduce((result, value, index) => {\n      if(index !== 0) {\n        result.push(<img key={`font_glyph_${index}`} style={{ height: '.9em', position: 'relative', top: '.1em' }} src={THE_FACE} />);\n      }\n      result.push(<span key={`${value}_${index}`} title={value}>{value}</span>)\n\n      return result;\n    }, [])\n  },\n};\n\nconst THE_FACE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJ4AAADMCAMAAAC1Heq1AAADAFBMVEUAAADm2c2bblM0RU3ZrpR9UzrTm4WPYUc6S1RKP0Kjc1d3TjPbsJ+Iemy6jXpNXGeuin0/NTOIXEC1kIS5fmbNo4aOZ1jInIAwNzmognitnJTp4dRXZW1nUkHsz8VPWFN3bmRcbHjMpI/grppqbmnCsaynY1SelpBkd4LjzMKUhHtZX1l2fH/jwK/jqpeKYUdxhZCFVzpveoGNkpBYUUfbtai7k4M7PTaFVzpCRT4pMCz118l7g4esgnjBnZPTtbBVUEXRtaqFWDvQpZiwhXlvcGTBlYXIhm87OzKXdmjnlYu2bV6Jm6GnppuabVOdsK0CAAAAAQwABBXgnGwYAgDgo3flq47Ih1rruI/Zp4/gqJLWppbhsJwBERzRm3vFjW7RpJC9iGTYq5zSkGHRl2zMjV/XlWXZm2zLlGn3upvpsZvprpTsqXngrZbfpo3LlHTispDisqTEjmQCDhTip4bMmnoWJTDCgVbhnXMSISjnpHHhom/8wqXWpYccLjkyJBo8LCDxroGSVzk+IBBJKhnztJSxdE+bXz7vvZbzsobSmnKlbEoDGCLXoI3kpnwgFBDsr47rsYXRoH/EjXnamnq7e1DmrIEOGyQsPUfrqILFlXeUaE0kNT/no3hJMyfyw6PZlW3gt531t4v+y7PmuqrNkHvYooO+jG4WDwnNm41WOinEg2b1xa73vI7NlYS7e1phOyIyFQava0ZvSDBXMhzksIewflymeWAkDQPYn3IqHBTgoILpsqXnuqHfpnyoblKkZELEmIfUrqK1hm2IUDGpe28iLjDNqZ6cZ0nXon1eR0TClIBwQybqwq/yuaVkRDC3hl6LbWZ8SivgqpzOjG7Zs6fDnZCugWSHX08NFBtCUl0gIyb+z8HJpJbYpHidb2PbqId7YFpXPzj1xrmBW0CycF16V0u3lo7edXioelaadWxwTz61jnHFjIfIb2prU064dEzEaV+2X0yqUEkYHh/IencyKyq6gXm8XFrhf4ZrOSDtp29UJQqtWDWae3a3aXGY31ZUAAAAUHRSTlMAC/7+/v3+/v7+/v3+FP7+/f7+/v7+/vz9/isd+yk390b648DTWf5N+nRt6MSd43H55OCMV+/traJ6yk+mmXzWyb7Fm8eWyfDm46/F5qndxgsn7SkAADTmSURBVHja3JfBitpQFIZHssksRNwFq40DFsEEClUQh6G4EWZT0OzvUwQu3E36AtnfLPI89x0SX2Nw3e9cTQemq06daelPrnMUwc//nPPHufn/1R9NF583q+lk0u9PJtPVypc3/4T6k9Vuu40fh8Ph/Xq9vvd6fLzfrCY3f1uwbbaPw9Pp9P37KYoidzodj8enp6Nz7n692WxWq7/o42SzfoQJHnSkgo2qyZsnR+2OLsJRKN/fyd5outsOQXLaNiBB5PGapslR89QgmEUuWm/eExC2xW6b3EZBXdS6NMbYxupCG3e0TZblBy+BdNDhpACu3qnN/flyN5vFw6iugqIOaqO1M7a0prR42RyguyhvmELoeJQ+v0uT+59hu72NjkZXoa7boCgNsrS0NNo0OXjZYS9nn9NfZBzcrgognPbf1sP+Ypskt3yeUrYKdNG2uhSpfJ97ZWJb5vHgE0DgtXPgBWG8hfDtAHvTXYJC54oy01VdFFVVKlvmaL8HCElTfYOpKb21FkBmIRQlu+nN22i6m8VxPGiFS9tC/hRGKa3LQwbeRdRnPGmvsuApxQAU8Hm1s2nv6mjs63KWxHHbDsKq1ixqIXTSWlOYPMs8jleWK6V4LqjKIpWrstTwnQHj9Pp8o0WaxGHcMkEVeEZD6C9tyqKw4OBXZ59qGpV1eHwBcAXvzFeF43TRv3aazFhXF1ROF3yOLok5ZLVvM89VhoHP7SWefZUr2AAET+i4QGwHs2vy9eYP6Sx20RNraJkicEwtfZWhApMJLNUhy85AZyrwRNJnEHmnby6IAYDh4G43utrQLR6SJA6ca3JxwdBTjyQbq0pkKDOJO0+WiX1AXfqMMrlKMjzAPk5RtYPxw7x/JetkJcIgqk1jYSmRYqYgY0X3qswy9YyHieftuDzzdB5PS2fl4H44GNylyysY2P+4nMXEVcBU1wQsbH7MYCpzuYMpQPLSqkPX3eyMxXtQplhqSs5lOSSOsA++Lw/z3h/2db5YpkkQRYGEakSG4NXBCzwJNgvYnprZuxh28dBT/cT0X8fqWlSIfbT37i6d/wndh09LdiIJnHGOfgTEnc67/GD85dFYoKiByKnQhcY/IPG048M/DJRDPwbj8fjLp9f7N/r6LeX+Dx14Lmgr6Wx2wetssuXhLOoOr7MObsSfzm9/6wh9JwLiBcD004dXB/HyW0oSB0cLnTV1bbtwe9ZB5eLUM7Co45MS5vMLQm2xL2jbEDzhkwanXz+8MonTlLSLouhI1hFsrIU48gKPyc9fIHd8/lXwOIygyBJKtVhHNIcc+F65wKP5Mp19GYf8mtSmzBXRZmX2X+BlPtR48SU2WBdSSh8tqASPfJFobuX2yILIfvR+/5fTA9Yl45j/IJzTBJ4zZDCOcL3AUEL9q7pGCx3yb5Nw0QViBqW94LEePzgxf9cmwjiMo6E0WMpBcSh6/ghUPLVDaF0EVzdFI4LT/RUHkSxRbIhDA53KFRo6hzqkbYKULHUQ6RAodPLS1c2lQ6bQwc/zvbv6q2lSH1MDhbafPM/7fd73vcWLxjv9cO4Bcp0sFxkRor7GE0D/LxbV3bk6xTP74EPKGBHvSLxL/9I9YptADlMLHpMLYl8Lz2elDbFpFJ7JV7zsgRQoNa941c43L0b38AEzL77sbFGdUgpnv3IH0x/iBY9Bja2XUTLJ4iuh0PDi8YWP8hubDj164NxypB6nEn1eXbf6EZtDgX9B4eJ4gVagvoKa0fk18WUTPpXz+HgPOZ7EeNnQl0prs4YXt9fF8QpRdDpGNfPOYi4zGUn9Uc7j0t2ec91bjutQ6vWaFVZYnyXfyArM8MZGS7uxf3qMDg3PF19RBW185Dtu+d2Y93IubQle7yvHDdVu6SjyI/BkgL1eXJCvX/gDjwJFoTYQ20KM79JYhbfo5ZAnvGzJl6zruH2BJ+P+R1GQVqDCTeyzdFEGwTfW9fLuguvmvDx42Wy5RgOAFzcrYGPgvULDOjrF83WQBa9ow9HLrOl2lHs8PcZW9iwPXj6fc7Nr5VqNjiJf+Mw7qTACDQ0h/C1dH6ljtL3pAIN6jjf6dnR98Xk+h3l5p1fXZaLMdusnR44xfNtNNATR+KLSkfEFOv7p8Jex81+m587dH7X87jx/mveg85xMWbsPeMUwGCdVwW1vLy1tmwSYMBrnKSs5hEc8EjT7Yjyd7nmn/kadna/fMzzoemugKYcS8dqnHpWq4CqVSrsiiZBX6uMpnjUzT63CSMOruyVohle3y9u1EeYJj7HAcD2g8P2IMQtE9nI4mtiAga7d3jK1T5YqSxKQKeMvvkjPeMOIdEP4eJWlunaPxVHZgpdj1NfKyHolCOJD+1A04CzWE9gGXXTY7W61221gY8Y0bfsJ+KIj+I6wj/sds1dGRfDUzvPn2XdtEbq852bxTgcfP7nSnOcbZLCdnAA3EFqs7haABwdAEnaMmAZdeNHoiy+KIguH5QNezOcu3D2nke/M5/NqPHUKxod2YUVnoQFnti1hm8HFbF9Mh4et7mDQmtrCUAARjB3ja7wCr0+6USCxAKPSVx46EDHT4Z1TftceQ4d59CR4NSZWYo8dUm4GVxEbziVs701G2J2amBoMBgAKsrJRYSGurIhxBfUbJKNKDTBCBVGXfTnsG75hiM7DPDWyzndSEPzFtRu/tpNhQCmcoX2QeAdwhi8wB1uDg63NzQoJK+NGQ3gd8NT2uof0i0jpZpz8UPumFxe0mSnbetEG1sxrpGgpnSTjsE6DikFd0BBk0meJd8O1qLutqVYLA20RdlZk4e7KCrNSKDT0iCMKS8bHk6vhW++1ec/zXMdRtjzm0VwYXqNhVMYkyYQ4VxZcq4sS4xK0nZ3qzjKAfGOf/+TjxMTEVOtgsx2vwc4uMrwX/PKC8Zl/6uahW+/9OQ5SOoFp6QWnmb5soCRLSX8BQhYccIcmGQec0GCrHh9XdwDkG4Ik6S8zMzMTUz/gs0HubHdkoT618NQOVHWxZA/Hnbmz7Zt+4jrkChyt4lvZmfQrMMvIkFZbOqqGBpulChs6Pj5ebTZXV6vV6jIS5P7795PrkzM42GItYOG7pbdvAaRpGmKMn+VT1LoAMx1Pps/OltO7rk4svOj3aWXQOh2xtU3aFBjGgcjEJiWhim119Q1qNoGsor3lz8v70uTM5LeJVqvFkGxYxtsAmoXxcR+Jj+6bP8u+S/cXXKaiB12yUaTmpXA2Bt1USjSxLYaDLkZ7/fojajY/frzabH7a27uyt79/+fLl9fX1ye8sQQFutjc23lGEKR58HMs5wcxmsu6Z3Tf9k247i4mrDMM4nqAjMiDCoCAal4sa45KYqIlLNPHGRC+80FoVqewU3ICiaKPiEK244AJFq4LBalqmRaVq7SAYREAEQR2IwqANLWClYTF4YdrEC+L/ec8cZsbqIzPSmtZf3vfbzjftjVcx8uDpIiVGxxyL6AwVqVnENmI0t63gsN1TWvrzz6UA8/MBfnjW72cF+xr6vCShNTHRk0yPQ6FQR0dbnV8j0OXZYeGtb8/5n3PzJVewHnN2fZsFxT2eRNdemukONbEi07RBEa3eSgeOgNv1s5LPV35l/lmFhUGlq8ubANA3Pe1ZXl4OzZvPeBaHd+jbt3+lfP/xiEHxmBinP8NBh+bG62ySrtdMMmhKHxPVBpyVjsKVFpfvUkrwVebt2bOnkioWnqWEu7q6jiRMT2dOLywsCNjW5meNcXnkfta/czi4bDjj5A2NJU+H/lfcxwp4TC50NFa7gk0CV4bKCfMUGAGHraSkZFd39xvk5co9eW+8kZe3RxUsLAwEAoOD4aGuI5lkenoa4CgFZAav++wozdULj70XndxbHVQ4HN5pT/POpLB12GoHzrUZjXrt3//Efs3Te0RTU4uFKynr7t62DR5p//LL9vbT9lRW5gfy85ubA6mDg4NDR4gJDdjm938qnuu73+42fr3mP+btmSx48HjmkU4ruqv7G13UpmZGSNh4N1tp8RZw8NBt2vSlUnHffRUV8uU379nTTE6cSD0xRCJCfB1MYJXPzZ18wMTicv3JQ88efh7nkOiePNm57ZRJa+Ghs7rRTNGqhauuLq2WDR2BV1am4m167bXHSMXdDz10X0XOq6cxAvM6yVhz6ph8aUNLSwZcWA7poBDHs63t2pOe0LgHfVePPdElhfUOXozOcPRUIqKxVuqE0m3ZItu27k2bTHc3eQgeBXz1NKWz87R9nWN//JF6IjstLdsBTsNr2+6Plo/u2tHlhvPjeRffKB4PeDHLcY/fjpqfmM76agPuQ7OpWFu2FOOz73eV7UK3kbY6vJ0O75H7SM6r7Qg7T4PX3/9HRmoayc5Wiymf+WJ4d9rh5e1r/sXbwCeh3/7COSCet106JoVw6NRWOmmiuJRtjMG9tnPnwYOmc3gVFbVOAeXrH0vNyMhAOMQ0njZfHeWLPqbrz0w8fu0Z8b298JRTzuHyOIb3Ojxn0qqxpgNHsKErUzNxWXZtFA8bkc54dz8S4eVUvEqonjLV359BUlPTBge7Epz6sbi4QN1c8fgR390LNiTx8QAXeOIZjq3W79++3e1s/brOKmfl2ogJFS4nTwmneWG6Knymq6C57VQQX0GBgP39x47Jl7ru87N3uEA9aP7ySjzvksvTz6G1EZuz5PWg+/x7cOiijV3XrUfOXRu3bRPMqrezqQmefPfZ0lLb3t5eW1FB/XIKCAWcgpcxNgbQ+tux3R+tH0AOB/G8y/hw5a8oT08rtpdpwZPOXegMV4yOUK5tG7e5RnjIlJ3vNFE9fOiUWZbmdglpcIHr6ydjzc2pQ5q+nzD8tHnE+E7iHXoUXtx1CQPvwDfPjjSYznAEm3S8wcOEjB1WPpXPmRcUz+luxd3kscewGU8+Cz7xCL6EowfsdBDDu+PfvFsP7eUcEKPrsWmR7DMdNuIuKIp84mlKwJNOfZWOGI8IR75ic6udzcmpzcnJKcjNzd03M2NjkKQODnVlekLyvb43yrvjr3/xdOh3eei0pnxyINmTyJR1ddCIUz1UkOiu5sa2MmxRHTbS6PrEm52drWCKiIfPqSDp3Ddmw4/Ti3wxu8cv18RNjVtf3ws+Wry67Wqth+LR2Vid8SIzlWza1g2P0ilfRXhNBxsbG6siODcIc0iuw3OF8g0lTNvm27PX9XG2vz52x73m+Ot7daHt6p4zXbJ0+6PDLlo9onUE3qbuXWVaThR0+Kx0ingHI7aDs4Ty0VwiWsGr9s++1BODXZnTq/PzbT17Xd/eXw5dG8u73q/i8aUFz9bjDnTfjPTVW+lkM5p0wvGCR+CV2Tb71WtffeW2tqmpsfHeRo0/s4lZVXU33VWMR1ynlhd2N52tePKI8A6lx24bZ1yLfL2zrzMtbOB90+DqzAbN4SndpkPEu7ngSYcPXNO99zY2HqSAzBDbfKvII+TUXEVGy6sFuQX9TN+lzNVRjqZRXlLsgfl8l7f3Lg55PTZrkz2+Zxve1PHEdCZzO7tRFbNOnpwmakfx8FU1Ymx8qPGhex8kmx/cTCQ8VUY3Rbn78GVPUL46eHdFjnJxvItu6HHczF+OURp5Ni00Z12eaCXFwjEjwDkWBcsHTfrW3lye8uC99z70InmgN5L3399sPIMVFf3EK7cAX9rS3Cq8HjY3Z79P33BelHfNrT3A7D5ApzwrXuJIQzCiKy8vKS7h/KTVDh2hbjAUKQB8wHfuj91U4eM//fieZffu3t6t+FJS8KUIx0vVky+D8ll3dQ1t+30s76LL/ZKZ7wu/v66t4/ujrQmL1lmHp5Tp1a3aPaXaQfnAaHp9fC884fgpgfXT0pnsI/LDDz/s3r1761bzFQ0PDxcpKVa+man+DCufX+0ltPDmc2OWvQ11ryNDx70COqbt0VZvcNyeDF2eCeGRJ6VzHB+bQ28WfqhmPvAA770v9q7rfvvNfJMDKQMDkykDMzMzw+BSjMcPpo5RPodHxLs2hnfuBtY9YteC8EKhZF/C4jiPEjbu1nkvl3STpzZpddPGah6nWrzsR/JBInwTrZ3xzDdsgQSQqHqUb4ryWXdxoIMXMzUuuNwPDB4Dr4fihZY9rd5wIbxiUoquPBKk3Ye/ItCks9BYquUGkeLahKN6ppucHBgwGZ4pFzhcYOVbm1tlY7N7NdUodlm++HJdaBF0fuMdTQiPi1eqta4cXTUprC6vDuS31OyoZQs4qFS5QLF4qbG4ohEuksnJSVXNLDqPZvRPzQwPDONjdqi7Nvjk4+WnetGnyCTxDMfE6OhY9vi8QXjMiy27Skh5deH4eDA4vmJJzTr77B0ystZGeYopVbinDffDe3JZZIPFQ0YGb9lrS0tDaRlTMwMDAuLrZ23hXGA8JP/Hq1PxDhxNXERXXl6KjH5WVo4vehOIt4scOTKUvbLSknV27amn4qtSXKGV8QGzOcDdaqpsx9LS1iYmJubmeIv8ey3t2PAwwCIyBW9U3SVixPIuvvA4Pn72OLyOkHobpLXIpOOKJJjQ6vMdVez2ZnVhYTohTB2zWMMcHkDTOcqIrRca02F4inpNzK2OzltGV+fmVlfnAK6tZR+jgAAHZjKG6G66v4dPZNTC62OnxoZ02s4/6DRvl32L41yLIOOrJbCYYBeHXF6HOixtbRqfy0czu1bGKGKOI4zEeALaNpFSNEPfMtnzuXAkbXXp6Umjo6ujAk6oglPDk5NFRTOszPCO+w8d6sGXHnugOu/2pDq/EultcmKwML+S1NTUrAQTqBs6u/onuhmu04NIB8Tp1q6VrLMZirOPxfoA9r5PTj2VJTd7YnphdV44Py94AsJbsD4vZU8NaLmx7qanH1fxTuKNwrMYz5MQDuSDI4HFRNk8HnS6tn7OfHYNrr1v2ZPoHUxtaQns+O6gElltxKui8QVTqSrdnHSRtIk3Lx08Cz6tNxlrc6NJ+pszh/jLM7fGnpYvuNGqZ7GZ4Q20CNdS0+J1S3cg5Hw8Bku4Or6hgAcOeBI1CgtbXj78mB2Um1wetTt7bGVo6Qi85Q5cRG8MPnAae+ItOb5JeOqufH6+0mN5592S7vDq/M+1dfB/7BJPvrDPB444PhOup41r+pB8i2HW8JcPs1hb5OulsQVZK10Jzk2ZDdf5+Q5oNFWxxgo3kTmRPazuZkysqnyW9IvibveS/NGxx34bDlQSdK0eQmtVQo+cnuTvTSnm5/D0NNfK8SFYWP7yYdtPDrNaV1X1vl9wdhY8dOKFQira8vLCQiYoy5oTEZcyhgfgrS3ASzfgDXG8S5IYt45OPO9gAFxeTbjVVhNfIgCurZURLq99vmS7XIcZCn3v4T+PeFknHR8v8ehslh7D9Mvssls07pWPsByncT2VnZ3GCp2hu6rstew0ht/MsWwGHz6A6fBicu7N8KSz04qP3orXvNiaiMu7SIJB3rx9Cv/mQxTP8gFLMjwehheD49Xl3RyiDx/eWaVo4A2GxSO+oz5omdyKdg0NpiqDGeSYE5CszzNTaQ7vHHg3xz3nngHPdAy9UDJDr6amsrJlkC0iHB5cCQTY0djTIHIVFJS0QR8B0G+f7xsfn+zhs/Z2b6K7ai4DD55+gwQCkVeC13474zWPkf6pKXcPnrLuThgPXRyPXJeOjYmhdcXTGs7Py6upDAyutKQGWpi++dWF7LmEu1G9B4P1fV6n2YnUjiQ01AefKC+J43EFEA6Hu7yEFoCEhy7Q3Iwuq59wJoBFtLe5c0NJ+hfvFodXtx0eQ68FXl5LSxajO2sHlcyvrpZQX0GHWd/XN6KAaxhpIPX1T1SXlOHbie6RzRzjqB7h0wzziec1HpXrzMrq79y3j6fJogEySTT4lv6PdyVzQ9ne4fLyOJPUsh9wNqmpLM/Pr4Y2zgn1zfEnxvWJXnCxj+ASju/q/3zinmLxNPQ2v58CLzU1IJ586u9Ignhq7lg/NyydXEcaTzZ9DXPmo7v/xbsswmuDp0U57zR8ObO1tbU7xMuH9+GH1dXj+9+sJ4AQ6WXvij6AKS0ue0rVE29rETxmwFln8XGVMxBUPqxDBGB/Jz6Vr2jrbmzE5enr5oviP9YYjfDUXGZGHips4rE4t8DjM7x79jfoA2QLLSWimVE6qreFhyRr7vubi3g+HGs2nnfERmkikwOdJTvtRPMe2kv9clO2bp2M8igdgRd/wZcey2M+7OBGjlA74fjYaX/918/qrpR5amOp1UrCuKJ09nmkmitek6pnvOZAOMggYK0EJ96RiI7D6NAJJki/1S9la0qUZxn9F+/qVTZEdHURXo14s6odOj4PKwzWN7zwDToKQaa1ivG2MJ3o9S5Su6///Ey8jfCa9ATZ6/AGw6xBzB99Vs+uo4V5bmKJrQIiCyBzxPGJx9S1ddlyeXxzb1qdly/dqhduifBm23dIx4QN9o28oPWNc9+yDn2M0dH50blRNilvsO+zPykfvC0bn3x450sfsOP2bs7dR/EC49RuJNHDRsgmWNc2r8rYQQ8g9Rv7g/Yaj6zzRpNG4cWdqCbgUTt4n/jC0tVyoVlrsyJgusRnfYk+NtzQ9k+f8/M03MPqmcRGupzo7fvM+Sj8nmJ4D7/0wfMv/ti7OacTHlWneL7v9WdE2g79dfycpKRfLxy1wx77Gz7NYWaHfJypxLOsbojnXXpbCB/hLC/ejrPh1bbnGe/D/aqdJzn0CWe8+bYvXue55Dh/m5nfZnU+dGDkd8aelU88mvs8j+CbX+1sDuQ3B4J93la25o5Pv+CkCY9fo1M9DQgtCNjM8JMvtrkU+LZ43j+Mm39M1GUcx5dDXGXN9NCszTVZNP+Q/qJ/XLVmy1V/OjWO2yGQ5g4OvAaDaaxkYYE7ArncsFGHcTCjoHDlilOsbIoZDu9UttK0OlyWZq5mI2m93p/vfT363dvjFnL5vHh/nh+f53k+36WP2v253NszLjzYenq2+7ns7CvBu4bjkziABUfOjd320uc/vfT57TfS1thvE8cmDw4PXHZuw9c9taliS5v49gUcvI+ZVW5++9jEkXfeee5Z8iThaQS8aofD4/Bd1OAlvqweLh7u3n/H9K637BHOd4VHp/pu1qmd0Dl4mMeIpRLl6LVruj39auz8+R9e+uDzl2jpts9v/O30hT2Tkw0sItzsgrc+jVceCBT2t5tOzZp8e8/8CSpfXnpW/9Nt5xihP7yqlNb8uxX7jA/3fnPxzoGXoVu0/FGKODDc3HPwtjM2RvzJp4kseMcnr6n45/kjF4RHdJ9VhnPu5Mkvjx69dnRrCXiXwSO4HRyoNXKe5wUP/2KehsnJm8fHVUBFKn7bc6+OnZ8zxlkt8cU98C7uZptreKQsdGYlrPPBy9AtvW/GLOw7Bh4ne698X1PTAx50VRueXlO7ddsB3KO2hvTz2MTY2JEP2HWyQH/w6ticL+cePz55dKu5p77XWbGlqam1sVF4V8HrjXkOHz8+c+44+yiKvN6g+bHftMsdZ16CjuDiHjK8ecJjZI89ev891+nuXJL7/ambXpm5J4O3XXh+f/OTG9Zxq/wyc14DxQRzZ+6h6I3MnN+cvPy77+bPnztX5Ul0POA071UYnuseivWm8mYhEsRvJr6b0Hw5NkZOShZ6VVPLRS0dNvk5GRV4E2MLFz6sIlzXvAW9ntRNs8SH9rzSm8ZrFh5aU0uCQk7C22GyvPG54xPfjH8zPp+F6jB6YZvjHSceK9X3HPeC2ZgHXjFlLHnD1NnMspQKw6C6KN3KF3eoRqfRe+j0nHPs1H+Yv/DWT/YSXXdcLO0dPpBKHZ5BjZPwZnyfwXuSEyoMpKbHM1DCFUJJigxlBkyWp+TlHWYR3oZ5GTzXvWA41m4q9mhR5pU3PJxKeTxX24VlgB+Cxj1q2MWbd05z/ScXP7w6/Pg9bmyX5Q40HHwhNbx31kxtxoTnZ9TWCK9MeOKjDoU0SpKJpjwgreosbR4ZAX2vSXjl5YHycH+sH/ewL5UyPkxMDXg8FGXEpH60OxyGL+wM3UOnzxPcHy7MO13Un8q7Nx3dO5c9Mjh1sGGrx0P/OwbeTUpYenAvbZ70NPnUgItnfAdUXCbr3OvKdWXMyp1bhNdaDl9ldr+D10d45bXMIwpUL/HX4GVDh7J1z2sn4YY3sfBkabf30pXH77g+6V2Zmpoa6Os9deqVPeDt3VnjHwGvZgOiUSmZ3EBZlNE51YNIbCqzqTU68GReR1NTW6vxebmdj4mjGED8Q/xmVH6ZsNCfLbp6vnIsLyg9dH6M2M4r7foskPzlcffpsEWPgXc5WkVmsvfmt9+euXcnyZ6mZJ2e6QoyXXWE+mqdfBRGBJxbOYXKHLwt1/EqxRfj1S7AgVQqtXWr7DO6qlgkuyccLuSqiHfwNLNcvHDkyIXToX2fjUZ+efyBO6bhranjFK/m47nHSFg402FKbtbI2FDm4sEXjSbVD2trgZRsOhGdiYvAugwenc9L4PqlmAgF5SnpKxEbvyhFQlaIU1np1VWbLRulJxf+cGThoVAoNOpNTcP7ZWqqLLruSY4FZhw9Ns6RUz14EfCk9YZX934UuihSZxSli4W/9rbe6Do6HDyzDz4ThCZWcE6WmpujUbFVVw9VV4LnDRfZ0ODAfuHEd3NKEyHfaGA63o9TV6JlT61LVtXMuPnmV4RXz5IhvKiELVIkqne+B1izjclu2WSx6Do7kK6FDC+BNXQsCIQpPCZ6XKtDnS0tHR26Daz0IhsYHEIemjMxMS+UCAVG4577777jFhsawqutW7/u6WTM/62DVwgd7l2Ho+XOlk7e6+ooa4gCZDIqxF8osuBtcfEESMOYo3fE3ioiGR6/B2pdXS75gt5gwvA+mndufF4ikQjE4yvuv/sWF+/SVLSzrkwVgd/OvHnGpzk5hWd7IpGR91UQZWxIfMIDBhoA9Uc39rp4hrlus/DQllbU2Cr3gmJLBBKBAJGmGm1IdEP6p4yutTxg8gV9PkLL2Dg/Nv/WcMLrjSceeeDuu1y8FVfqKuqiyb7B4W1zv9mbszansJK+sX8Vt9ybN/GvVVSoU7WIETxY1rvavFl4nXV4B56517RrV2NjY/lqmRcMJoKBOHjio69JhJUXcI2N3B6N7hsd5a6S4QDgR+cvzP+k3RvwBvIfW37ndby+2rrOaHLgwMGDLzTMPZWzlqIYlQCsQjCRhdCqqYLvNlk4XTbo8E0OrxIeIplHhFbyJRK+OHTwrRafBkQHMjrgpNf2QQde6MRHpy98mefJprvm5y6/444bnGk5NxmtK1tzmbTpOCXG32Ne4VnhwdfSgnPgIRibOkCQg4ibtc2/bkb4u0p41IkQtMZdu3a9BV7AyruKEsKTRuFTaUGH0bUBB90zzs3MaCgthu6XeakVsXA4P3cpj3W6eHWday5PkRrh3qydWeCZe/CpkwDWJLVuacJFaDs3bRaew4Y61T+tQqmFZnGPoCnj8xYKLxQSHSFkFLS6gk7mwSbt02UlOnFojvCS/mzwlmGePT2f29xZVzs1yB8Ib1qwdq2Lx303cKIzte1q0nfwEVWB2bvT/VRkM9TSQbuttEvLgQBoRaXgiU5qBJoftza1NhndM5jn4HH7AV83ePM+SV1ihGbwuHTJ9a+K1g5OXTlwhRLpmxYUZhUWeisJhN13gwOfCAkb78SYLoiDLh5waTzRWbsInHgiwXUU7hmeCUCTRRYZ3NdvcrclQjrfyauXwCvZqeAanqWjZwzvyhVW+fe2F+qqhzIUSirAaxOe2N7ijzwEDwMrGCOMEoN08ejwcu4Zaxi8eCjElbcbXOD0U1cO3Ouvm3vI+D46+enPl35OelZk8NhFLh5ZtQa8wSsHtg2XcFNGQQKnYO++Cxx4aTpJBtogER6MwAmPSfkp4bXKFJM12x0PzS6CL45ER3gzeo3PSF8bn4t3enfs0qW+DJ7sW7J4e0sZePS+rQMl9RuzjG41eBQvKJxNBif/ZKB1PxMh1uBYSYUBeB1tLp01yw0zDRbtJrwOHx0y7Zt9yuC++EK/iIt3YvbaoiHlbStyc5fcaXg2dBdURy9PoUGyPvC6oHPck5p2NZl1DiN80/Gc3ic8JpU0njX7BXy0qd5nePsCAd3hu/Yiw4OPD4rPhsfaQCV0HsObthc6W1c7CN1gX7Jke1fhxi7DQ23QuHHN4InP4Gx0iI5KJQutmaNWxSfAUJHDF0Duzx29ycfcD7pX+aGiQNiTGlBsl4Hn7iQX5+8vk32DyUhJzcYuJLyN3MIblXGhTOdD4IlOikZlntu6i4fAUzEIfIbXiHlu/N3gSuoH1vdml3rZZvRl5hXetBnKH4quGYSvNlq8PUt4Pl2BTseTjUaXwatw8FTaauPC2s7gWbtMt4nShE+rWrnmk4x9f6QzceLbfzWViuXn5y5ZdB3vhkUPLT5bbXyDfSX+rC6u6xgcqK1tmnuZmc8kOsc6Op5WUSa8v+LRcoiFzefgtWbwgEsDquu5eLvbr66IkREsXwZepvMtrq+ORNe9PDhYq+ByTeyDj673BFwOnSQ85JrnhBY6FjOUCW7GFolWXb5M53vT+IzudeGxru0LdRVRchguj+fnLlqkgZs5xliQPUQmvGZNsrgKPEVXN/DvgufwGZr1PJtpbGw4oSXR77BVykL3lpp3XUE2PEI+nyVV9om0w05w9QKvm4tz6Gav/TSRiMfz8x+AzoUDD/uyh2iHTUBVL8GVfxvLhfciYNCZRIeEN73nrWxhwkONLGm7RIA3hvWZ0dF4iMoz4aX5/hBcZwIXXJfwQvSF/Py7b8h454zdBf7kk5FoS3WkN2sfn5Z7hFfFPS4ecHLP+AzP+t56Ol6jLVcqeFSqRBbHejUa7453U2Bjq4cvEfQpY5HFf+594HE3HeryzfaxBHZ3+/Lzl+NdRhq7S+FrjjxZ3TFEcLvwTysH8UWZABsefLhnfIYXBU/GNarWcUhZQWAUseIyn9Df3/wsTnDV9yTjs1mPec+khAr5stiIFxWwTNPzXLqMfbmLs2uaI9XVIzU2sYAXdPEswm5sFVmUwVsPnupEK/2xqlhPdYf2uNCNxsN6fCSc4L/coUu+58Q3MzbAtJqNLsMrLegO5efeDdEf8TR4F++sGhmKjNQUAhcyPJcPNNO0UcsXfU94ZXUdJOmx9mEe1zzcF4lUr64sV7IX/vLo5NGZe9u9cfASLl5rBi+drrh4Vm0YKihYfrcT2r+Gd4F/JOKvqZd5dGbvRsl1DzSjE5npV2Wiwlu5fygSSzVMcvp8raGvOaLNP2pv4PvJg6n+RMKbSGg/Bp/qSOmcz4wK0IETnRp0CvoeXG7e/dk+wvsQfDWR5pqcrq4T4NGC8BgbqK2pzaYU8AyN16+/ggdgWVk0kuzLO3gNTTZ4kv6IznYqw/0QTx5tSLWzFU9o0Q2uNrWKD71maK53PgevQHnUX6W5me6XU+OvqlnQZSWKhV7v2cJ0/RGJFXLpjC2NRzalp/oGhl84SPn/toE+ledwOJbN+djVvMN5KQ/fhOl78UAwmMYLOHzAjY66FaXC4yDD7Xf/5J/wREceyX5NfJnU6k90ii7dL7qOZyJ1/jSc8hT3Ca+4OJat3WB/OyehnFFBF8c+n+HZ+UHjKGAMn1HNx+Zd0BkZS/7BOybn9PA1PH6VQqRSd/CoQYaOddWBA88FhLBuvfAGUhyMccJW3FeMqmLZHF+Ew/0XOaAKFxFbpMEBnhRgbAcCQnPwGIdFWRxj5C76W7rM8M3xG15WlvDq61WLv5rx1sauSFtKN4GH7NdN8FVsqujU+QKAfcJLxpJViKNFDY/wbhS2fC/0Jz4EX1x0AfAQeAVLIPlHmX05C3K69mVhduHaHPCMT3m94VkOasI44CTtcKNJRxHkN/UIrwg+4NgM8XL4vKvLDdDo0m8E1+vNKj1R8CDm/Yus+913kjm5Kwu8tcLj2rQa7agGL50g64UqTG0tnCWsikQ4rYRtKDLUg1w8xE6t2xF82AceDmYYGRUoweljwYNL7/wXthtcvo+stC1LeDTl8rl4SENWfG0VO1p28IgLi1nEleFx4VroLfQGddcYkgwPQOwTHYguH1H1zUbQPaQM+b/9W1wAnuyr345GegxP7nW4eJxhtGxqYSOnr/37OVDAPgdvhHM8bgr9Z9kuW2EtZQzINdAXhM8kA4OIGvAQ97nQsfv5V7b08vFQQQF48NVzAO7n6m8IOh554LSKsNpaRsUFZKDt2LH/zPtn3tdJajOKrBqqqvKDV3PWHn6gZtrKQ13CkOFV2vwnD72Gp9tmh+5/8T1YMNvBqwcP+3p04mJ4kGlntpJqH0oudpwBTOWlT29AAMpBphV/cUmV8Oi+lPwCCCL3tSeIso/ZT8dz8EkcAdKQmSe6/8eHfaXMLOCNECv8gw48jqBWWgKqapoz1JqdabZ6Zipw0oWw4K0qtueFixkbQeHxzJxqqE9SKHVaZejY5+CVG53hBX0MC0bt/xLT34PCy4KvR3wjwkPQAWd47zdv2NBb0kuZAdIbeNBp5ILmr/L0+SuDwuNxgvPzVGVG8e2cD08zkl08qdLB6wKP9eJ/atHvxZ1dqMtxGMdTTImTMEKkI0RxOheWG0c4lMSNvGzTzoaDki3LIjq3FLkxLqwUF7N0iqKsdYRZ4jQXQrjxcuHteAtrHW7w+T6//RE3xvA9+//nmIuP5/m9/H/P73l+m453h8q5wlPn6HHmA88WF+x3xEhQIZ1Kt5ish2uPHVLV8KHbODfRHcvGsU2YVCrshixpbzjJP9FwHDm8kDTweNearfXg0XtnX14nvEeGR/ProXtSeKOqPsElSbPePwS8PvA6aHsbCod2Hu3pOaIRxrpu4sQWDWoscsOXlSGvVG+lFL4qgocMTwU6WK9ry6pZ86f8Kh5Bl/nzZoejTLmGF8G9vTU8dAya00o/67sbA+9Gh/VYhVGxHZsBPJvGqWwW3XmJsJ3y+AHEwaUrTVEPD4EXwrngzazj/Eca32XNucxovTQ+w0MOb9NmPHtR+YRk/ZNSeNCx41bNaj02pKFVoR4e+0K3WHoRPFF63p0yvm0qE/Z/hHNNVh5rePLtr3t33uWoZz1nvgsWalZRJK5N0ub6aHaxTIrNy+3a4LrdzXbgu3fdt7UftSqOj7PVapWdZU26SAYsFv2kfUUNT3wYkamFyqFQaPKUes6lBA/nKlvErJeiSyrqr+j2pn2O7xym60j1MoNltd+45/3bdwjA7urtrNDevOH2ub+/H0QyBcL3i+Wi368c9jjy8NR1160NTh5fD96UeVfXCe8gnntgbavX4yvg3KT4UC7iY4J4MhW9f/8BNtOTN6CZwIPv85WxJNMU+/N5yhSUIeN2scBDA0MalWctrOtoT2JC64LkPoLHvIAiuAyxU7QJPJmvbz+lWLncuWukzFUqlfcf3iNDtJvoPr/5bCLhlkL1wY+Vre3rij8i8mpjn/CCtL3w0GZ8W4fGzWwO+/HEGOFZlRBz/YleNpzA08BCcos2aa5ds8MeKuLjBd8HMN9xF+YH+ET32Q5JGDGC9PqOCF6g5i/EYyQ/NvPFu7Y2T6wLj3Bz1PBS+/YVmFSZTbWhqN5BfeSGztWHyTW8yYkxdlRGpbKXC4nSLvgQfB/MfC/Mfnfzo8gK6t2umsRQsAvZA7l2xCfXeazslPniG5MCD2PxAyAWpKiUKW19547Db8mscgefTJUq3/AkY8PLYqtW+xGJeiQ+MB3TyUIMxkHgTLyFwjS9ujRxoeFlEtBhPdkPwAI91/Bsw/60W6CNqiJaGt70RMvj9fz584989Bqp8x7QnniK5+gegYHHCwlz1pR6z4Fua77v96cyCa8IzCEWCqrf3MjuPEreTigNpCN76uPH7MfnCCRdSGhO1Y9V0R1wiQXiO+gzKnpeje8AC7T6NADvFsekMoblCVSujRgPdQq3N9LRLb6sw/sezKOToIPPB19KipCFgnyRgxGfD9Yovq1T4xY23/Hwkl/VmexERtepFsmo55J6YPge7iuiDc5p5XExwwEYMbyD3KBDKeE1tw34cdnzC+YLjMng3O/g8CcXKRhidL2FNS1waRqf9JWQS5BVoyNhCjyyVYRnSpGTojdeKX9XVMNKvXzj2poDZQ/Pk6MDDzrVSwovQc5RddQTNyaD6Ak0hj0yuk4h4JDPd8KnGdyKpMToS40haWbhsHrh0JRl4GVI5s/FkriwBscVg090CPN1qPURxHj7llEZwJrUldWt+0kyTJ/yBUNIXVUZnynBye68gdfc9uPXKP0K3rA28ODSMzt0sViMRcXqZKdenaqFNTzVJ55Gh9+S3gqg6QOzB2zQCa9MiCoYAjDu8CAzZbJZ6IrFxePqx0PTFgfSYEHHFZMwnHrHZtfwCshmuNWnSVgSHjuuFdPUylQkvFz6lB7NQuDBx3CH8RivkGWhgRdo+6nh/RLegLZAk9Ao8zLt0A0X02XdGPgAPvA6kzvsSLYziCxYtLcC3kXgStApsXarCT78S+/FcpkM2WmZU2Uz3u9p3OIm6MArYT2h2W1zYl9CnbbwgKcr8GCFDzyPDrxLBseQI78GITOZ/TQ8Z6Q0KiM6xm+qLYD1DM+1vW5sR3hR2VX7rMxZziVhCf9iQABVLqGkQ4Ki3ekscPRYZti1HqDcC17W8AQYKAeY0H5T09rpFLESeN0xSXQOT659AB14mzZSdc9QUyM8oxw9ZhICpAYX2kpQ3iMMBQ2vXBae1FT+bd+q9b3CetTPgPY9HnzAkYWz6ZuOuTRTl56XjcBG/DskOvDWSmQEiU9zb42OepgAM8YfmK/E8WrgeXBGhwhgKEnIKtmRbb+wxrQkSR68fCfiXQfiHp3QDA++sOHJek2wNY1cPO1PvvpjbmkUeNysZxieCnWEx7pjtzulYOXRFeKzdJGCZQ46sq2KLXp4RH9EaHjKyeUoJQHOxXi/r3HtoAE3inUZ3VfGY0oHzwQgL/AU8dPhOxJ4euDEpQii49uktduGCg/fcsiJnqx0WlZTYCEt7080o32URLwiJkBzrk8rJBN4EnE/AHfuOsqvF1hMKGwnMH5kNgCHQqfgmbqGjtl5nRZeYO64P/6GraWUI/FiXWt4mRqeARrcLvDICNqJGRF0rMGCW9eyq2l0CsvDh/HovYpYQ8dZaCMbQYd7ZyzN588Jb1SNL+Xzs/wF0OMDT0kZwtOZCrS9R+Ah4Dw8+AzP6IoB4bW3/Tmd7Ld8iAOk4jFnNWKYD8Dv8YjpmvE2FR7wwQnFHhF0QjRpz0zx6pN3hDeyf3i7Th5vBF+L+PJ9YizlAKRwssZ3wcPjJhU2FHoPKWKk1Y74vD6L9W5Bx/kXBNOgG+noGsZn6svLfmn46L7wyXoi9MQTFkpYfWXQRIzVDXnsdIeBk+meOTpGlMbxWYWolYjCVx6Di2WkB18NyIiH9pE6vjmRMrxoVzQqwFukCBiaH7gyQb6G2s7jsxpMr0Y0nYZP498hgnvqEIVN+0wJS+xmDeE3RU2Q+YsIOATcyMWi+0t8d2VBThbJgBeRhwvOcChBUB6R3U5xL1LADImM+VXV4FYP3j4NuoZqwpzR1GCqZNMqqFGuI4WYf23LhVa3WVW8HTyCgS4ZYrnMS2zQEVsWoQaURmvAhDmD7wFYa4FSyQgSqYTW58Al9Uyo2aWUyyB18bKdj8J1pURkfqw03Cuiarj9Wq2KVYR5qWbDmpKQIc0u6j05Kq9fqbQ+f8UTVZui8/zacL6WBa0jDFAWNMgh+VLphqY7gZn6ZNlzkMu+Vlf/UOKYBK5PY9uZx/6Whk1qmbOglVp69RI7soDjJnReAfMJYHZ+AeCQg642kOdfEXm0I1BBXNI+d9rfo/MIaybkghAWBAvVYfpFVdb6D1j5M+cDP6aezY64hW4uHfavawKA8HmSLe0daN1H6PCMEVgXjR5NtfEg+HTaxJI6/NqIRgiKYLgBJLljPaRBfEwx4D3+4iWVh9At+Wd0uFiArSNaW4Uz6LGQaoei6GZ8+kBwQD9tXbBgUQvVF/9OAM6hmwAIomO7jgBE/PksvzrM1gVz6kBrJOEkh1jD4wwUqMx4jtTBtUz6b18uPYyvvm7xCCHz/MqtVRIcbP9RGFF+xoyIN6mlpYXvNuc+6f9/d7gHqe9c595YT34BwJb8NsVLvTgAAAAASUVORK5CYII=';\n"
  },
  {
    "path": "client/src/components/Memeify/FontFaces/VaporWave.js",
    "content": "import React from 'react';\n\nconst charToFullWidth = char => {\n\tconst c = char.charCodeAt( 0 )\n\treturn c >= 33 && c <= 126\n\t\t? String.fromCharCode( ( c - 33 ) + 65281 )\n\t\t: char\n}\n\nexport default {\n  container: {\n\t\toverflow: 'hidden',\n\t},\n  editorStyle: {},\n  text: {\n    fontFamily: 'Segoe UI,Helvetica,Arial',\n  },\n\tpreviewOverrides: {\n\t\ttransform: 'rotate(39deg)',\n    height: '7rem',\n    paddingLeft: '2rem',\n\t  margin: '-2rem 0',\n\t},\n  textRender: (text) => {\n    const formattedText = text.toLowerCase().split('').map((char) => {\n      const c = char.charCodeAt( 0 )\n      return (c >= 33 && c <= 126) ? String.fromCharCode(c + 65248) : char\n    }).join('');\n\n    // TODO: Inline the path\n    const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`\n    return (\n      <svg viewBox=\"0 0 500 160\" style={{ height: '10em' }}>\n        <path id={id} fill=\"transparent\" d=\"M6,150C49.63,93,105.79,36.65,156.2,47.55,207.89,58.74,213,131.91,264,150c40.67,14.43,108.57-6.91,229-145\" />\n        <text x=\"10\">\n          <textPath xlinkHref={`#${id}`}>\n            {formattedText}\n          </textPath>\n        </text>\n      </svg>\n    );\n  },\n};\n"
  },
  {
    "path": "client/src/components/Memeify/RichDraggable/index.js",
    "content": "import React, { Component } from 'react';\nimport Draggable from 'react-draggable';\n\nlet body;\ntry {\n  body = document.body;\n} catch(e) {}\n\nexport default class RichDraggable extends Component {\n  constructor(props) {\n    super(props);\n\n    this.contents = React.createRef();\n    this.state = {\n      height: 0,\n      width: 0,\n    };\n  }\n\n  componentDidMount() {\n    const height = this.contents.current.offsetHeight;\n    const width = this.contents.current.offsetWidth;\n\n    this.setState({\n      height,\n      width,\n    });\n  }\n\n  render() {\n    const me = this;\n\n    const {\n      props,\n      state,\n    } = me;\n\n    const {\n      height: bottom,\n      width: right,\n    } = props.bounds;\n\n    const bounds = {\n      //top: 0,\n      //left: 0,\n      right: right - state.width,\n      bottom: bottom - state.height,\n    };\n\n    return (\n      <Draggable {...props} bounds={bounds} offsetParent={body} cancel=\".no-drag\">\n        <div ref={me.contents} style={{ border: '4px dashed rgba(0, 0, 0, .7)', cursor: 'move', position: 'absolute' }} className=\"creatifyDecor\">\n          <div style={{ border: '4px dashed rgba(255, 255, 255, .8)', margin: '-5px -3px -3px -5px', padding: '15px' }} className=\"creatifyDecor\">\n            <div className=\"no-drag\" style={{ position: 'relative', cursor: 'auto' }}>\n              {props.children}\n            </div>\n          </div>\n        </div>\n      </Draggable>\n    );\n  }\n};\n"
  },
  {
    "path": "client/src/components/Memeify/index.js",
    "content": "import { library } from '@fortawesome/fontawesome-svg-core'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\n\nimport React, { Component } from 'react';\nimport Select from 'react-select'\n\nimport RichDraggable from './RichDraggable';\nimport EditableFontface, { PRESETS as FontPresets } from './EditableFontface';\n\nimport {\n  faFont,\n  faMinusCircle,\n  faPlusCircle,\n} from '@fortawesome/free-solid-svg-icons';\n\nconst getRasterizedCanvas = (contents, width, height) => {\n  return new Promise((resolve) => {\n    // Parse to xHTML for SVG/foreignObject rendering\n    contents = new XMLSerializer().serializeToString(\n      new DOMParser().parseFromString(contents, 'text/html')\n    );\n\n    // Resolves a bug in Chrome where it renders correctly, but\n    // replaces the inline styles with an invalid `background-clip`.\n    if(/Chrome/.test(navigator.userAgent)) {\n      contents = contents.replace(/background\\-clip:(\\s*text\\s*)[;$]/g,\n        (match, group) => (`-webkit-background-clip:text;${match}`)\n      );\n    }\n\n    // Fix busted SVG images in Safari\n    if(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {\n      contents = contents.replace(/\\<img\\s/g, '<xhtml:img ');\n    }\n\n    // Attempt to match font kerning with the DOM.\n    const kerningAndPadding = '<style>svg{font-kerning:normal}body{padding:0;margin:0}</style>';\n    let svgContents = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width * 2}\" height=\"${height * 2}\">\n<foreignObject x=\"0\" y=\"0\" width=\"${width * 2}\" height=\"${height * 2}\" externalResourcesRequired=\"true\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>${kerningAndPadding}</head><body>${contents}</body></html>\n</foreignObject></svg>`;\n\n    const pixelRatio = 2;\n\n    let img = document.createElement('img');\n    let canvas = document.createElement('canvas');\n\n    img.height = canvas.height = height * pixelRatio;\n    img.width = canvas.width = width * pixelRatio;\n    canvas.style.height = `${height}px`;\n    canvas.style.width = `${width}px`;\n\n    let shadowNode = document.createElement('div');\n    Object.assign(shadowNode.style, {\n      height: 0,\n      overflow: 'hidden',\n      width: 0,\n    });\n    document.body.appendChild(shadowNode);\n\n    shadowNode.appendChild(img);\n\n    var svg64 = btoa(unescape(encodeURIComponent(svgContents)));\n    var b64Start = 'data:image/svg+xml;base64,';\n    var image64 = b64Start + svg64;\n    img.addEventListener('load', () => {\n      window.requestAnimationFrame(() => {\n        // We still can't trust Firefox's %$%&* engine, add another 5ms timeout\n        // `background-clip: text` is very broken and does not always render in time.\n        setTimeout(() => {\n          let context = canvas.getContext('2d', { alpha: false });\n          context.clearRect(0, 0, canvas.width, canvas.height);\n          context.fillStyle = 'white';\n          context.imageSmoothingEnabled = false;\n          context.scale(pixelRatio, pixelRatio);\n          context.fillRect(0, 0, canvas.width, canvas.height);\n          context.drawImage(img, 0, 0);\n\n          document.body.removeChild(shadowNode);\n\n          resolve(canvas);\n        }, 10);\n      });\n    });\n    img.src = image64;\n  });\n};\n\nexport default class Memeify extends Component {\n  constructor(props) {\n    super(props);\n\n    const fontKeys = Object.keys(FontPresets);\n\n    this.contents = React.createRef();\n\n    const fontOptions = fontKeys.map(\n      (fontName) => (\n        {\n          value: fontName,\n          label: (\n            <div style={{ maxHeight: '150px', maxWidth: '100%', fontSize: '16px' }}>\n              <EditableFontface key={fontName} fontFace={FontPresets[fontName]} preview={true} value={fontName} editable={false} blinkSelection={false} />\n            </div>\n          ),\n          fontName,\n        }\n      )\n    );\n\n    this.state = {\n      activeElement: false,\n      bounds: {},\n      fontName: fontKeys[0],\n      elements: [],\n      fontOptions,\n    };\n  }\n\n  componentDidMount() {\n    // TODO: Fix bounds\n    /*\n    const bounds = this.contents.current.getBoundingClientRect();\n\n    this.setState({\n      bounds,\n    });\n\n    console.log({\n      bounds\n    })\n    */\n  }\n\n  setActiveElement(activeElement) {\n    this.setState({ activeElement });\n  }\n\n  addElement() {\n    const {\n      state\n    } = this;\n\n    const newElementKey = `element-${state.elements.length}-${Date.now()}`;\n\n    const newElement = (\n      <RichDraggable key={newElementKey} bounds={state.bounds} onStart={() => this.setActiveElement(newElement)}>\n        <EditableFontface fontFace={FontPresets[state.fontName]} value=\"Start Typing!\" />\n      </RichDraggable>\n    );\n\n    this.setState({\n      elements: [...state.elements, newElement],\n      activeElement: newElement,\n    });\n  }\n\n  removeElement() {\n    const {\n      state\n    } = this;\n\n    const activeElementIndex = state.elements.indexOf(state.activeElement);\n\n    if(state.elements.length === 0 || activeElementIndex === -1) {\n      return;\n    }\n\n    const elements = [...state.elements];\n    elements.splice(activeElementIndex, 1)\n\n    this.setState({\n      activeElement: false,\n      elements,\n    });\n  }\n\n  async onSave() {\n    const renderedCanvas = await this.renderContentsToCanvas();\n\n    if(this.props.onSave) {\n      this.props.onSave(renderedCanvas);\n    }\n  }\n\n  async renderContentsToCanvas() {\n    const me = this;\n\n    const contentsElement = me.contents.current;\n    let contents = contentsElement.outerHTML;\n\n    // Cheap border/handles removal\n    contents = `<style>.creatifyDecor{border-color:transparent!important;background-color:transparent!important}</style>` + contents;\n\n    const contentsWidth = contentsElement.offsetWidth;\n    const contentsHeight = contentsElement.offsetHeight;\n\n    // Fix the dimensions, fixes when flex is used.\n    contents = `<div style=\"height:${contentsHeight}px;width:${contentsWidth}px\">${contents}</div>`;\n\n    return await getRasterizedCanvas(contents, contentsWidth, contentsHeight);\n  }\n\n  render() {\n    const me = this;\n    const {\n      props,\n      state,\n    } = this;\n\n    // TODO: Abstract into separate package & use CSS Modules.\n    const spacerCss = { width: '.3em' };\n    return (\n      <div style={{ position: 'relative', flex: props.flex === true ? 1 : props.flex, display: props.flex ? 'flex' : 'block' }}>\n        <div className={props.toolbarClassName} style={{ alignItems: 'center', color: '#fff', display: 'flex', padding: '.3em', position: 'absolute', top: 0, left: 0, right: 0, background: '#333', flexDirection: 'row', zIndex: 2 }}>\n          <FontAwesomeIcon icon={faPlusCircle} size=\"2x\" onClick={() => this.addElement()} />\n          <div style={spacerCss} />\n          <FontAwesomeIcon icon={faMinusCircle} size=\"2x\" onClick={() => this.removeElement()} />\n          <div style={spacerCss} />\n          <div style={{ flex: 1 }}>\n            <Select style={{ flex: 1 }} isSearchable={false} options={state.fontOptions} onChange={(option) => this.setFont(option.fontName)} />\n          </div>\n          <div style={spacerCss} />\n          <div onClick={() => this.onSave()} style={{ alignItems: 'center', alignSelf: 'stretch', border: '1px solid #fff', borderRadius: '4px', color: '#fff', display: 'flex', padding: '0 0.6em' }}>\n            <span>Save</span>\n          </div>\n        </div>\n        <div ref={me.contents} style={{ fontSize: '22px', overflow: 'hidden', transform: 'translateZ(0)', flex: 1 }}>\n          {state.elements}\n          {props.children}\n        </div>\n      </div>\n    );\n  }\n\n  setFont(fontName) {\n   this.setState({\n     fontName,\n   });\n  }\n};\n"
  },
  {
    "path": "client/src/components/NavBar/index.jsx",
    "content": "import React from 'react';\nimport SpaceBetween from '@components/SpaceBetween';\nimport Logo from '@components/Logo';\nimport SiteDescription from '@containers/SiteDescription';\nimport NavigationLinks from '@containers/NavigationLinks';\n\nclass NavBar extends React.Component {\n  render () {\n    return (\n      <div className={'nav-bar'}>\n        <SpaceBetween >\n          <Logo />\n          <NavigationLinks />\n        </SpaceBetween>\n      </div>\n    );\n  }\n}\n\nexport default NavBar;\n"
  },
  {
    "path": "client/src/components/NavBarChannelOptionsDropdown/index.jsx",
    "content": "import React from 'react';\n\nfunction NavBarChannelDropdown ({ channelName, handleSelection, defaultSelection, VIEW, LOGOUT }) {\n  return (\n    <div className={'nav-bar-link link--nav'}>\n      <select\n        type='text'\n        id='nav-bar-channel-select'\n        onChange={handleSelection}\n        value={defaultSelection}\n      >\n        <option id='nav-bar-channel-select-channel-option'>{channelName}</option>\n        <option value={VIEW}>View</option>\n        <option value={LOGOUT}>Logout</option>\n      </select>\n    </div>\n  );\n}\n\nexport default NavBarChannelDropdown;\n"
  },
  {
    "path": "client/src/components/PageLayout/index.jsx",
    "content": "import React from 'react';\n\nimport SEO from '@containers/SEO';\nimport NavBar from '@components/NavBar';\n\nclass PageLayout extends React.Component {\n  render () {\n    return (\n      <div className={'page-layout'}>\n        <SEO\n          pageTitle={this.props.pageTitle}\n          pageUri={this.props.pageUri}\n          asset={this.props.asset}\n          channel={this.props.channel}\n        />\n        <NavBar />\n        <div className={'content'}>\n          {this.props.children}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default PageLayout;\n"
  },
  {
    "path": "client/src/components/PageLayoutShowLite/index.jsx",
    "content": "import React from 'react';\n\nimport SEO from '@containers/SEO';\n\nclass PageLayoutShowLite extends React.Component {\n  shouldComponentUpdate () {\n    return false;\n  }\n  render () {\n    return (\n      <div className={'page-layout-show-lite'}>\n        <SEO pageTitle={this.props.pageTitle} asset={this.props.asset} />\n        <div className={'content'}>\n          {this.props.children}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default PageLayoutShowLite;\n"
  },
  {
    "path": "client/src/components/ProgressBar/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport ActiveStatusBar from '../ActiveStatusBar';\nimport InactiveStatusBar from '../InactiveStatusBar';\n\nclass ProgressBar extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      bars       : [],\n      index      : 0,\n      incrementer: 1,\n    };\n    this.createBars = this.createBars.bind(this);\n    this.startProgressBar = this.startProgressBar.bind(this);\n    this.updateProgressBar = this.updateProgressBar.bind(this);\n    this.stopProgressBar = this.stopProgressBar.bind(this);\n  }\n  componentDidMount () {\n    this.createBars();\n    this.startProgressBar();\n  }\n  componentWillUnmount () {\n    this.stopProgressBar();\n  }\n  createBars () {\n    const bars = [];\n    for (let i = 0; i <= this.props.size; i++) {\n      bars.push({isActive: false});\n    }\n    this.setState({ bars });\n  }\n  startProgressBar () {\n    this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300);\n  };\n  updateProgressBar () {\n    let index = this.state.index;\n    let incrementer = this.state.incrementer;\n    let bars = this.state.bars;\n    // flip incrementer if necessary, to stay in bounds\n    if ((index < 0) || (index > this.props.size)) {\n      incrementer = incrementer * -1;\n      index += incrementer;\n    }\n    // update the indexed bar\n    if (incrementer > 0) {\n      bars[index].isActive = true;\n    } else {\n      bars[index].isActive = false;\n    };\n    // increment index\n    index += incrementer;\n    // update state\n    this.setState({\n      bars,\n      incrementer,\n      index,\n    });\n  };\n  stopProgressBar () {\n    clearInterval(this.updateInterval);\n  };\n  render () {\n    return (\n      <div className=\"progress-bar__wrapper\">\n        {this.state.bars.map((bar, index) => bar.isActive ? <ActiveStatusBar key={index} /> : <InactiveStatusBar key={index}/>)}\n      </div>\n    );\n  }\n};\n\nProgressBar.propTypes = {\n  size: PropTypes.number.isRequired,\n};\n\nexport default ProgressBar;\n"
  },
  {
    "path": "client/src/components/PublishDescriptionInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\nimport ExpandingTextArea from '@components/ExpandingTextArea';\n\nconst PublishDescriptionInput = ({ description, handleInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Description:'} />\n      }\n      content={\n        <ExpandingTextArea\n          id='publish-description'\n          className='textarea textarea--primary textarea--full-width'\n          rows={1}\n          maxLength={2000}\n          style={{ maxHeight: 200 }}\n          name='description'\n          placeholder='Optional description'\n          value={description}\n          onChange={handleInput}\n        />\n      }\n    />\n  );\n};\n\nexport default PublishDescriptionInput;\n"
  },
  {
    "path": "client/src/components/PublishFinePrint/index.jsx",
    "content": "import React from 'react';\n\nconst PublishFinePrint  = () => {\n  return (\n    <p className={'text--extra-small text--secondary'}>\n      By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.com/learn'>Read more.</a>\n    </p>\n  );\n};\n\nexport default PublishFinePrint;\n"
  },
  {
    "path": "client/src/components/PublishLicenseInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\nimport { LICENSES } from '@clientConstants/publish_license_urls';\n\nconst PublishLicenseInput = ({ handleSelect, license }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'License'} />\n      }\n      content={\n        <select\n          type='text'\n          name='license'\n          id='publish-license'\n          value={license}\n          onChange={handleSelect}\n        >\n          <option value=''>Unspecified</option>\n          {\n            LICENSES.map(function(item, i){\n              return <option key={item + 'license key'} value={item}>{item}</option>;\n            })\n          }\n        </select>\n      }\n    />\n  );\n};\n\nexport default PublishLicenseInput;\n"
  },
  {
    "path": "client/src/components/PublishLicenseUrlInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\nimport { CC_LICENSES } from '@clientConstants/publish_license_urls';\n\nconst PublishLicenseUrlInput = ({ handleSelect, licenseUrl }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'License Url'} />\n      }\n      content={\n        <select\n          type='text'\n          name='licenseUrl'\n          id='publish-license-url'\n          value={licenseUrl}\n          onChange={handleSelect}\n        >\n          <option value=''>Unspecified</option>\n          {\n            CC_LICENSES.map(function(item, i){\n              return <option key={item.url} value={item.url}>{item.value}</option>\n            })\n          }\n        </select>\n      }\n    />\n  );\n};\n\nexport default PublishLicenseUrlInput;\n"
  },
  {
    "path": "client/src/components/PublishNsfwInput/index.jsx",
    "content": "import React from 'react';\nimport RowLabeled from '@components/RowLabeled';\nimport Label from '@components/Label';\n\nconst PublishNsfwInput = ({ nsfw, handleInput }) => {\n  return (\n    <RowLabeled\n      label={\n        <Label value={'Mature:'} />\n      }\n      content={\n        <input\n          className='input-checkbox'\n          type='checkbox'\n          id='publish-nsfw'\n          name='nsfw'\n          checked={nsfw}\n          onChange={handleInput}\n        />\n      }\n    />\n  );\n};\n\nexport default PublishNsfwInput;\n"
  },
  {
    "path": "client/src/components/PublishPreview/index.jsx",
    "content": "import React from 'react';\nimport HorizontalSplit from '@components/HorizontalSplit';\nimport Dropzone from '@containers/Dropzone';\nimport PublishDetails from '@containers/PublishDetails';\nimport PublishTitleInput from '@containers/PublishTitleInput';\nimport Row from '@components/Row';\n\n// this class seems more like PublishForm and should probably be renamed\n\nclass PublishPreview extends React.Component {\n  render () {\n    const { isUpdate, uri } = this.props;\n    return (\n      <div className={'publish-form'}>\n        <div className={'publish-form__title'}>\n          <Row>\n            {isUpdate && uri && (<p className='text--secondary'>{`Editing ${uri}`}</p>)}\n            <PublishTitleInput />\n          </Row>\n        </div>\n        <HorizontalSplit\n          collapseOnMobile\n          leftSide={<Dropzone />}\n          rightSide={<PublishDetails />}\n        />\n      </div>\n    );\n  }\n};\n\nexport default PublishPreview;\n"
  },
  {
    "path": "client/src/components/PublishUrlMiddleDisplay/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nfunction UrlMiddle ({publishInChannel, selectedChannel, loggedInChannelName, loggedInChannelShortId}) {\n  if (publishInChannel) {\n    if (selectedChannel === loggedInChannelName) {\n      return <span id='url-channel' className='publish-url-text'>{loggedInChannelName}:{loggedInChannelShortId} /</span>;\n    }\n    return <span id='url-channel-placeholder' className='publish-url-text tooltip'>@channel<span\n      className='tooltip-text'>Select a channel below</span> /</span>;\n  }\n  return (\n    <span id='url-no-channel-placeholder' className='publish-url-text tooltip'>xyz<span className='tooltip-text'>This will be a random id</span> /</span>\n  );\n}\n\nUrlMiddle.propTypes = {\n  publishInChannel      : PropTypes.bool.isRequired,\n  loggedInChannelName   : PropTypes.string,\n  loggedInChannelShortId: PropTypes.string,\n};\n\nexport default UrlMiddle;\n"
  },
  {
    "path": "client/src/components/Row/index.jsx",
    "content": "import React from 'react';\n\nclass Row extends React.Component {\n  render () {\n    return (\n      <div className={'row'}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n\nexport default Row;\n"
  },
  {
    "path": "client/src/components/RowLabeled/index.jsx",
    "content": "import React from 'react';\n\nclass RowLabeled extends React.Component {\n  render () {\n    return (\n      <div className={'row-labeled'}>\n        <div className={'row-labeled-label'}>{this.props.label}</div>\n        <div className={'row-labeled-content'}>{this.props.content}</div>\n      </div>\n    );\n  }\n}\n\nexport default RowLabeled;\n"
  },
  {
    "path": "client/src/components/SocialShareLink/index.jsx",
    "content": "import React from 'react';\n\nclass SocialShareLink extends React.Component {\n  render () {\n    return (\n      <div className={'space-between social-share-link'}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n\nexport default SocialShareLink;\n"
  },
  {
    "path": "client/src/components/SpaceAround/index.jsx",
    "content": "import React from 'react';\n\nclass SpaceAround extends React.Component {\n  render () {\n    return (\n      <div className={'space-around'}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n\nexport default SpaceAround;\n"
  },
  {
    "path": "client/src/components/SpaceBetween/index.jsx",
    "content": "import React from 'react';\n\nclass SpaceBetween extends React.Component {\n  render () {\n    return (\n      <div className={'space-between'}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n\nexport default SpaceBetween;\n"
  },
  {
    "path": "client/src/components/VerticalSplit/index.jsx",
    "content": "import React from 'react';\n\nclass VerticalSplit extends React.Component {\n  render () {\n    return (\n      <div className={'vertical-split'}>\n        {this.props.top}\n        {this.props.bottom}\n      </div>\n    );\n  }\n}\n\nexport default VerticalSplit;\n"
  },
  {
    "path": "client/src/constants/asset_display_states.js",
    "content": "export const LOCAL_CHECK = 'LOCAL_CHECK';\nexport const UNAVAILABLE = 'UNAVAILABLE';\nexport const ERROR = 'ERROR';\nexport const AVAILABLE = 'AVAILABLE';\n"
  },
  {
    "path": "client/src/constants/channel_action_types.js",
    "content": "export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';\nexport const CHANNEL_LOGIN_CHECK = 'CHANNEL_LOGIN_CHECK';\nexport const CHANNEL_LOGOUT = 'CHANNEL_LOGOUT';\n"
  },
  {
    "path": "client/src/constants/channel_create_action_types.js",
    "content": "export const CHANNEL_CREATE_UPDATE_NAME = 'CHANNEL_CREATE_UPDATE_NAME';\nexport const CHANNEL_CREATE_UPDATE_PASSWORD = 'CHANNEL_CREATE_UPDATE_PASSWORD';\nexport const CHANNEL_CREATE_UPDATE_STATUS = 'CHANNEL_CREATE_UPDATE_STATUS';\nexport const CHANNEL_AVAILABILITY = 'CHANNEL_AVAILABILITY';\nexport const CHANNEL_CREATE = 'CHANNEL_CREATE';\n"
  },
  {
    "path": "client/src/constants/confirmation_messages.js",
    "content": "export const SAVE = 'Everything not saved will be lost. Are you sure you want to leave this page?';\n"
  },
  {
    "path": "client/src/constants/publish_action_types.js",
    "content": "export const FILE_SELECTED = 'FILE_SELECTED';\nexport const FILE_CLEAR = 'FILE_CLEAR';\nexport const METADATA_UPDATE = 'METADATA_UPDATE';\nexport const CLAIM_UPDATE = 'CLAIM_UPDATE';\nexport const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';\nexport const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';\nexport const ERROR_UPDATE = 'ERROR_UPDATE';\nexport const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';\nexport const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';\nexport const THUMBNAIL_NEW = 'THUMBNAIL_NEW';\nexport const PUBLISH_START = 'PUBLISH_START';\nexport const CLAIM_AVAILABILITY = 'CLAIM_AVAILABILITY';\nexport const SET_UPDATE_TRUE = 'SET_UPDATE_TRUE';\nexport const ABANDON_CLAIM = 'ABANDON_CLAIM';\nexport const SET_HAS_CHANGED = 'SET_HAS_CHANGED';\n"
  },
  {
    "path": "client/src/constants/publish_channel_select_states.js",
    "content": "export const LOGIN = 'Existing';\nexport const CREATE = 'New';\n"
  },
  {
    "path": "client/src/constants/publish_claim_states.js",
    "content": "export const LOAD_START = 'LOAD_START';\nexport const LOADING = 'LOADING';\nexport const PUBLISHING = 'PUBLISHING';\nexport const SUCCEEDED = 'SUCCEEDED';\nexport const FAILED = 'FAILED';\nexport const ABANDONING = 'ABANDONING';\n"
  },
  {
    "path": "client/src/constants/publish_license_urls.js",
    "content": "export const CC_LICENSES = [\n  {\n    value: 'CC Attr. 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by/4.0/legalcode',\n  },\n  {\n    value: 'CC Attr-ShareAlike 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by-sa/4.0/legalcode',\n  },\n  {\n    value: 'CC Attr-NoDerivatives 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by-nd/4.0/legalcode',\n  },\n  {\n    value: 'CC Attr-NonComm 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by-nc/4.0/legalcode',\n  },\n  {\n    value: 'CC Attr-NonComm-ShareAlike 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode',\n  },\n  {\n    value: 'CC Attr-NonComm-NoDerivatives 4.0 Int',\n    url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode',\n  },\n];\n\nexport const LICENSES = ['Public Domain', 'Other', 'Copyright', 'Creative Commons'];\n\nexport const PUBLIC_DOMAIN = 'Public Domain';\nexport const OTHER = 'other';\nexport const COPYRIGHT = 'copyright';\nexport const CREATIVE_COMMONS = 'Creative Commons';\n"
  },
  {
    "path": "client/src/constants/show_action_types.js",
    "content": "// request actions\nexport const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';\nexport const HANDLE_SHOW_HOMEPAGE = 'HANDLE_SHOW_HOMEPAGE';\nexport const REQUEST_ERROR = 'REQUEST_ERROR';\nexport const REQUEST_UPDATE = 'REQUEST_UPDATE';\nexport const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';\nexport const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';\nexport const SPECIAL_ASSET_REQUEST_NEW = 'SPECIAL_ASSET_REQUEST_NEW';\nexport const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';\n\n// asset actions\nexport const ASSET_ADD = 'ASSET_ADD';\nexport const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';\nexport const ASSET_UPDATE_CLAIMDATA = 'ASSET_UPDATE_CLAIMDATA';\nexport const ASSET_REMOVE = 'ASSET_REMOVE';\n\n// channel actions\nexport const CHANNEL_ADD = 'CHANNEL_ADD';\n\nexport const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';\nexport const CHANNEL_CLAIMS_UPDATE_SUCCEEDED = 'CHANNEL_CLAIMS_UPDATE_SUCCEEDED';\n\n// asset/file display actions\nexport const FILE_REQUESTED = 'FILE_REQUESTED';\nexport const FILE_AVAILABILITY_UPDATE = 'FILE_AVAILABILITY_UPDATE';\nexport const DISPLAY_ASSET_ERROR = 'DISPLAY_ASSET_ERROR';\nexport const TOGGLE_DETAILS_EXPANDED = 'TOGGLE_DETAILS_EXPANDED';\n"
  },
  {
    "path": "client/src/constants/show_request_types.js",
    "content": "export const CHANNEL = 'CHANNEL';\nexport const ASSET_LITE = 'ASSET_LITE';\nexport const ASSET_DETAILS = 'ASSET_DETAILS';\nexport const SPECIAL_ASSET = 'SPECIAL_ASSET';\n"
  },
  {
    "path": "client/src/containers/AssetBlocked/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\nimport { selectAsset } from '../../selectors/show';\n\nconst mapStateToProps = (props) => {\n  const {show} = props;\n  const asset = selectAsset(show);\n  return {\n    asset,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/AssetBlocked/view.jsx",
    "content": "import React from 'react';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\nimport HorizontalSplit from '@components/HorizontalSplit';\n\nclass BlockedLeft extends React.PureComponent {\n  render () {\n    return (\n      <div>\n        <img className={'asset-blocked__image'} src={'/assets/img/451sign.svg'} alt={'451 image'} />\n      </div>\n    );\n  }\n}\n\nclass BlockedRight extends React.PureComponent {\n  render () {\n    return (\n      <div className={'asset-blocked__text'} >\n        <p>In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.</p>\n        <p><a href={'https://lbry.com/faq/dmca'} >Click here</a> for more information.</p>\n      </div>\n    );\n  }\n}\n\nclass AssetBlocked extends React.Component {\n  componentDidMount () {\n    /*\n    This function and fetch exists to send the browser the appropriate 451 error.\n     */\n    const { asset } = this.props;\n    const { claimData: { contentType, outpoint } } = asset;\n    let fileExt;\n    if (typeof contentType === 'string') {\n      fileExt = contentType.split('/')[1] || 'jpg';\n    }\n    const sourceUrl = `${createCanonicalLink({ asset: asset.claimData })}.${fileExt}?${outpoint}`;\n    fetch(sourceUrl)\n      .catch();\n  }\n\n  render () {\n    return (\n      <div>\n        <HorizontalSplit\n          collapseOnMobile\n          leftSide={<BlockedLeft />}\n          rightSide={<BlockedRight />}\n        />\n      </div>\n    );\n  }\n}\n\nexport default AssetBlocked;\n"
  },
  {
    "path": "client/src/containers/AssetDisplay/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\nimport { fileRequested } from '../../actions/show';\nimport { selectAsset } from '../../selectors/show';\n\nconst mapStateToProps = (props) => {\n  const {show} = props;\n  // select error and status\n  const error  = show.displayAsset.error;\n  const status = show.displayAsset.status;\n  // select asset\n  const asset = selectAsset(show);\n  //  return props\n  return {\n    error,\n    status,\n    asset,\n  };\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onFileRequest: (name, claimId) => {\n      dispatch(fileRequested(name, claimId));\n    },\n  };\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/AssetDisplay/view.jsx",
    "content": "import React from 'react';\nimport Row from '@components/Row';\nimport ProgressBar from '@components/ProgressBar';\nimport { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\nimport FileViewer from '@components/FileViewer';\nimport isBot from 'isbot';\nimport Img from 'react-image';\n\nclass AvailableContent extends React.Component {\n  render () {\n    const {contentType, sourceUrl, name, thumbnail} = this.props;\n    switch (contentType) {\n      case 'image/jpeg':\n      case 'image/jpg':\n      case 'image/png':\n      case 'image/gif':\n      case 'image/svg+xml':\n        return (\n          <Img\n            src={[\n              sourceUrl,\n              '/assets/img/default_thumb.jpg',\n            ]}\n            alt={name}\n            className={'asset-image'}\n          />\n        );\n      case 'video/mp4':\n        return (\n          <video\n            className='asset-video'\n            controls poster={!!thumbnail && thumbnail || '/assets/img/default_thumb.jpg'}\n          >\n            <source\n              src={!!sourceUrl && sourceUrl}\n            />\n            <p>Your browser does not support the <code>video</code> element.</p>\n          </video>\n        );\n      case 'text/markdown':\n\n        return (\n          (isBot(window.navigator.userAgent))\n            ? (\n              <img\n                className='asset-image'\n                src={'/assets/img/default_thumb.jpg'}\n                alt={'markdown available on page load'}\n              />\n            )\n            : <div className={'asset-document'}><FileViewer sourceUrl={!!sourceUrl && sourceUrl}/></div>\n        );\n      default:\n        return (\n          <Img\n            src={[\n              thumbnail,\n              '/assets/img/default_thumb.jpg',\n            ]}\n            alt={name}\n            className={'asset-image'}\n          />\n        );\n    }\n  }\n}\n\nclass AssetDisplay extends React.Component {\n  componentDidMount () {\n    const { asset: { claimData: { name, claimId } } } = this.props;\n    this.props.onFileRequest(name, claimId);\n  }\n  render () {\n    const { status, error, asset } = this.props;\n    const { name, claimData: { claimId, contentType, thumbnail, outpoint, pending } } = asset;\n    // the outpoint is added to force the browser to re-download the asset after an update\n    // issue: https://github.com/lbryio/spee.ch/issues/607\n    let fileExt;\n    if (typeof contentType === 'string') {\n      fileExt = contentType.split('/')[1] || 'jpg';\n    }\n    const sourceUrl = `${createCanonicalLink({ asset: asset.claimData })}.${fileExt}?outpoint=${outpoint}`;\n    return (\n      <div className={'asset-display'}>\n        {(status === LOCAL_CHECK) &&\n        <div>\n          <p>Checking to see if Spee.ch has your asset locally...</p>\n        </div>\n        }\n        {(status === UNAVAILABLE) &&\n        <div>\n          <p>Sit tight, we're searching the LBRY blockchain for your asset!</p>\n          <ProgressBar size={12} />\n          <p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.com/faq/what-is-lbry'>Learn more.</a></p>\n        </div>\n        }\n        {(status === ERROR) && (\n          pending ? (\n            <div>\n              <p>This content is pending confirmation on the LBRY blockchain. It should be available in the next few minutes, please check back or refresh.</p>\n            </div>\n          ) : (\n            <div>\n              <Row>\n                <p>Unfortunately, we couldn't download your asset from LBRY.  You can help us out by sharing the following error message in the <a className='link--primary' href='https://chat.lbry.com' target='_blank'>LBRY discord</a>.</p>\n              </Row>\n              <Row>\n                <p id='error-message'><i>{error}</i></p>\n              </Row>\n            </div>\n          )\n        )}\n        {(status === AVAILABLE) &&\n        <AvailableContent\n          contentType={contentType}\n          sourceUrl={sourceUrl}\n          name={name}\n          thumbnail={thumbnail}\n        />\n        }\n      </div>\n    );\n  }\n}\n\nexport default AssetDisplay;\n"
  },
  {
    "path": "client/src/containers/AssetInfo/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\nimport { selectAsset } from '../../selectors/show';\n\nconst mapStateToProps = (props) => {\n  const {show} = props;\n  // select asset\n  const asset = selectAsset(show);\n  const editable = Boolean(\n    asset &&\n    asset.claimData &&\n    asset.claimData.channelName &&\n    props.channel.loggedInChannel.name === asset.claimData.channelName\n  );\n  //  return props\n  return {\n    asset,\n    editable,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/AssetInfo/view.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport Label from '@components/Label';\nimport RowLabeled from '@components/RowLabeled';\nimport SpaceBetween from '@components/SpaceBetween';\nimport AssetShareButtons from '@components/AssetShareButtons';\nimport ClickToCopy from '@components/ClickToCopy';\nimport siteConfig from '@config/siteConfig.json';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\nimport AssetInfoFooter from '@components/AssetInfoFooter/index';\nimport { createPermanentURI } from '@clientutils/createPermanentURI';\nimport ReactMarkdown from 'react-markdown';\n\nconst { details: { host } } = siteConfig;\nconst { serving } = siteConfig;\nconst { markdownSettings: { escapeHtmlDescriptions, skipHtmlDescriptions, allowedTypesDescriptions } } = serving;\nclass AssetInfo extends React.Component {\n  render () {\n    const { editable, asset } = this.props;\n    const { claimViews, claimData } = asset;\n    const {\n      channelName,\n      claimId,\n      channelShortId,\n      description,\n      name,\n      fileExt,\n      contentType,\n      host,\n      certificateId,\n      license,\n      licenseUrl,\n      transactionTime\n    } = claimData;\n\n    const canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId: asset.shortId }});\n    const assetCanonicalUrl = `${host}${canonicalUrl}`;\n    // Todo Issue #882 centralize all this media type detection\n    // Todo get markdown settings from siteConfig\n    const embedable = contentType.split('/')[0] === 'image' || contentType === 'video/mp4';\n\n    let channelCanonicalUrl;\n    if (channelName) {\n      const channel = {\n        name   : channelName,\n        shortId: channelShortId,\n      };\n      channelCanonicalUrl = `${createCanonicalLink({channel})}`;\n    }\n    return (\n      <div className='asset-info'>\n        { description && (\n          <RowLabeled\n            label={<Label value={'Description'} />}\n            content={\n              <div className='asset-info__description'>\n                <ReactMarkdown\n                  className={'markdown-preview'}\n                  escapeHtml={escapeHtmlDescriptions}\n                  skipHtml={skipHtmlDescriptions}\n                  allowedTypes={allowedTypesDescriptions}\n                  source={description}\n                />\n              </div>\n            }\n          />\n        )}\n        {editable && (\n          <RowLabeled\n            label={<Label value={'Edit'} />}\n            content={<Link className='link--primary' to={`/edit${canonicalUrl}`}>{name}</Link>}\n          />\n        )}\n        {channelName && (\n\n          <RowLabeled\n            label={\n              <Label value={'Channel'} />\n            }\n            content={\n              <span className='text'>\n                <Link className='link--primary' to={channelCanonicalUrl}>{channelName}</Link>\n              </span>\n            }\n          />\n        )}\n        <SpaceBetween>\n          {claimViews ? (\n            <RowLabeled\n              label={\n                <Label value={'Views'} />\n              }\n              content={\n                <span className='text'>\n                  {claimViews}\n                </span>\n              }\n            />\n          ) : null}\n          {license && (\n            <RowLabeled\n              label={\n                <Label value={'License'} />\n              }\n              content={\n                <div className='text'>\n                  {licenseUrl ? (\n                    <a className={'link--primary'} href={licenseUrl} target={'_blank'}>{license}</a>\n                  ) : (\n                    <span>{license}</span> )}\n                </div>\n              }\n            />\n          )}\n        </SpaceBetween>\n        <RowLabeled\n          label={\n            <Label value={'Share'} />\n          }\n          content={\n            <AssetShareButtons\n              name={name}\n              assetUrl={assetCanonicalUrl}\n            />\n          }\n        />\n\n        <RowLabeled\n          label={\n            <Label value={'Link'} />\n          }\n          content={\n            <ClickToCopy\n              id={'short-link'}\n              value={assetCanonicalUrl}\n            />\n          }\n        />\n        {embedable && (\n          <RowLabeled\n            label={\n              <Label value={'Embed'} />\n            }\n            content={\n              <div>\n                {(contentType === 'video/mp4') ? (\n                  <ClickToCopy\n                    id={'embed-text-video'}\n                    value={`<iframe src=\"${host}/video-embed${canonicalUrl}\" allowfullscreen=\"true\" style=\"border:0\"></iframe>`}\n                  />\n                ) : (\n                  <ClickToCopy\n                    id={'embed-text-image'}\n                    value={`<img alt=\"${name}\" src=\"${assetCanonicalUrl}.${fileExt}\" />`}\n                  />\n                )}\n              </div>\n            }\n          />\n        )}\n        <RowLabeled\n          label={\n            <Label value={'LBRY URI'} />\n          }\n          content={\n            <ClickToCopy\n              id={'lbry-permanent-url'}\n              value={`${createPermanentURI(asset)}`}\n            />\n          }\n        />\n\n        <SpaceBetween>\n          <a\n            className='link--primary'\n            href={`${assetCanonicalUrl}.${fileExt}`}\n          >\n            Direct Link\n          </a>\n          <a\n            className={'link--primary'}\n            href={`${assetCanonicalUrl}.${fileExt}`}\n            download={`${name}.${fileExt}`}\n          >\n            Download\n          </a>\n          <a\n            className={'link--primary'}\n            href={`https://open.lbry.com/${createPermanentURI(asset)}`}\n            download={name}\n          >\n            LBRY URL\n          </a>\n          <a\n            className={'link--primary'}\n            target='_blank'\n           href={`https://lbry.com/dmca/${claimId}`}\n          >\n            Report\n          </a>\n        </SpaceBetween>\n        <AssetInfoFooter />\n      </div>\n    );\n  }\n};\n\nexport default AssetInfo;\n"
  },
  {
    "path": "client/src/containers/AssetTitle/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\nimport { selectAsset } from '../../selectors/show';\n\nconst mapStateToProps = (props) => {\n  const { claimData: { title } } = selectAsset(props.show);\n  return {\n    title,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/AssetTitle/view.jsx",
    "content": "import React from 'react';\n\nconst AssetTitle = ({ title }) => {\n  return (\n    <h2 className='asset-title'>{title}</h2>\n  );\n};\n\nexport default AssetTitle;\n"
  },
  {
    "path": "client/src/containers/ChannelClaimsDisplay/index.js",
    "content": "import { connect } from 'react-redux';\nimport { onUpdateChannelClaims } from '../../actions/show';\nimport View from './view';\n\nconst mapStateToProps = ({ show, site: { defaultThumbnail } }) => {\n  // select channel key\n  const request = show.requestList[show.request.id];\n  const channelKey = request.key;\n  // select channel claims\n  const channel = show.channelList[channelKey] || null;\n  // return props\n  return {\n    channelKey,\n    channel,\n    defaultThumbnail,\n  };\n};\n\nconst mapDispatchToProps = {\n  onUpdateChannelClaims,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/ChannelClaimsDisplay/view.jsx",
    "content": "import React from 'react';\nimport AssetPreview from '@components/AssetPreview';\nimport Row from '@components/Row';\nimport ButtonSecondary from '@components/ButtonSecondary';\nimport { createGroupedList } from '../../utils/createGroupedList.js';\n\nclass ChannelClaimsDisplay extends React.Component {\n  constructor (props) {\n    super(props);\n    this.showNextResultsPage = this.showNextResultsPage.bind(this);\n    this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);\n  }\n  showPreviousResultsPage () {\n    const { channel: { claimsData: { currentPage } } } = this.props;\n    const previousPage = parseInt(currentPage) - 1;\n    this.showNewPage(previousPage);\n  }\n  showNextResultsPage () {\n    const { channel: { claimsData: { currentPage } } } = this.props;\n    const nextPage = parseInt(currentPage) + 1;\n    this.showNewPage(nextPage);\n  }\n  showNewPage (page) {\n    const { channelKey, channel: { name, longId } } = this.props;\n    this.props.onUpdateChannelClaims(channelKey, name, longId, page);\n  }\n  render () {\n    const {channel: {claimsData: {claims, currentPage, totalPages}}, defaultThumbnail} = this.props;\n    if (claims.length > 0) {\n      return (\n        <div>\n          <div>\n            <div className={'channel-claims-display'}>\n              {claims.map(claim => (\n                <AssetPreview\n                  defaultThumbnail={defaultThumbnail}\n                  claimData={claim}\n                  key={claim.claimId}\n                />\n              ))}\n            </div>\n          </div>\n          <Row>\n            {(currentPage > 1) &&\n            <ButtonSecondary\n              value={'Previous Page'}\n              onClickHandler={this.showPreviousResultsPage}\n            />\n            }\n            {(currentPage < totalPages) &&\n            <ButtonSecondary\n              value={'Next Page'}\n              onClickHandler={this.showNextResultsPage}\n            />\n            }\n          </Row>\n        </div>\n      );\n    } else {\n      return (\n        <p>There are no claims in this channel</p>\n      );\n    }\n  }\n}\n\nexport default ChannelClaimsDisplay;\n"
  },
  {
    "path": "client/src/containers/ChannelCreateForm/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\nimport {\n  updateChannelAvailability,\n  updateChannelCreateName,\n  updateChannelCreatePassword,\n  createChannel,\n} from '../../actions/channelCreate';\n\nconst mapStateToProps = ({channelCreate: { name, password, error, status }}) => {\n  return {\n    name,\n    password,\n    error,\n    status,\n  };\n};\n\nconst mapDispatchToProps = {\n  updateChannelAvailability,\n  updateChannelCreateName,\n  updateChannelCreatePassword,\n  createChannel,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/ChannelCreateForm/view.jsx",
    "content": "import React from 'react';\nimport ChannelCreateNameInput from '@components/ChannelCreateNameInput';\nimport ChannelCreatePasswordInput from '@components/ChannelCreatePasswordInput';\nimport ButtonPrimary from '@components/ButtonPrimary';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\nimport ProgressBar from '@components/ProgressBar';\n\nclass ChannelCreateForm extends React.Component {\n  constructor (props) {\n    super(props);\n    this.handleNameInput = this.handleNameInput.bind(this);\n    this.handlePasswordInput = this.handlePasswordInput.bind(this);\n    this.handleSubmit = this.handleSubmit.bind(this);\n  }\n  cleanseNameInput (input) {\n    input = input.replace(/\\s+/g, '-'); // replace spaces with dashes\n    input = input.replace(/[^A-Za-z0-9-]/g, '');  // remove all characters that are not A-Z, a-z, 0-9, or '-'\n    return input;\n  }\n  cleansePasswordInput (input) {\n    input = input.replace(/\\s+/g, ''); // replace spaces\n    return input;\n  }\n  handleNameInput (event) {\n    let value = this.cleanseNameInput(event.target.value);\n    if (!value) {\n      this.props.updateChannelCreateName('error', 'Please enter a channel name');\n    } else {\n      this.props.updateChannelAvailability(value);\n    }\n    this.props.updateChannelCreateName('value', value);\n  }\n  handlePasswordInput (event) {\n    let value = this.cleansePasswordInput(event.target.value);\n    if (!value) {\n      this.props.updateChannelCreatePassword('error', 'Please enter a password');\n    } else {\n      this.props.updateChannelCreatePassword('error', null);\n    }\n    this.props.updateChannelCreatePassword('value', value);\n  }\n  handleSubmit (event) {\n    console.log('handling submit');\n    event.preventDefault();\n    this.props.createChannel();\n  }\n  returnErrors () {\n    if (this.props.name.error) {\n      return this.props.name.error;\n    }\n    if (this.props.password.error) {\n      return this.props.password.error;\n    }\n    return null;\n  }\n  render () {\n    const { name, password, status } = this.props;\n    const formError = this.returnErrors();\n    return (\n      <div>\n        { !status ? (\n          <form className=\"form-group\" onSubmit={this.handleSubmit}>\n            <ChannelCreateNameInput\n              value={name.value}\n              error={name.error}\n              handleNameInput={this.handleNameInput}\n            />\n            <ChannelCreatePasswordInput\n              value={password.value}\n              handlePasswordInput={this.handlePasswordInput}\n            />\n            <FormFeedbackDisplay errorMessage={formError} />\n            <ButtonPrimary\n              type={'submit'}\n              value={'Create Channel'}\n              onClickHandler={this.handleSubmit}\n            />\n          </form>\n        ) : (\n          <div>\n            <span className={'text--small text--secondary'}>{status}</span>\n            <ProgressBar size={12} />\n          </div>\n        )}\n      </div>\n    );\n  }\n}\n\nexport default ChannelCreateForm;\n"
  },
  {
    "path": "client/src/containers/ChannelLoginForm/index.js",
    "content": "import { connect } from 'react-redux';\nimport { updateLoggedInChannel } from '../../actions/channel';\nimport { updateSelectedChannel } from '../../actions/publish';\nimport View from './view';\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onChannelLogin: (name, shortId, longId) => {\n      dispatch(updateLoggedInChannel(name, shortId, longId));\n      dispatch(updateSelectedChannel(name));\n    },\n  };\n};\n\nexport default connect(null, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/ChannelLoginForm/view.jsx",
    "content": "import React from 'react';\nimport request from '../../utils/request';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\nimport ChannelLoginNameInput from '@components/ChannelLoginNameInput';\nimport ChannelLoginPasswordInput from '@components/ChannelLoginPasswordInput';\nimport ButtonPrimary from '@components/ButtonPrimary';\n\nclass ChannelLoginForm extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      error   : null,\n      name    : '',\n      password: '',\n    };\n    this.handleInput = this.handleInput.bind(this);\n    this.loginToChannel = this.loginToChannel.bind(this);\n  }\n  handleInput (event) {\n    const name = event.target.name;\n    const value = event.target.value;\n    this.setState({[name]: value});\n  }\n  loginToChannel (event) {\n    event.preventDefault();\n    const params = {\n      method : 'POST',\n      body   : JSON.stringify({username: this.state.name, password: this.state.password}),\n      headers: new Headers({\n        'Content-Type': 'application/json',\n      }),\n      credentials: 'include',\n    };\n    request('auth', params)\n      .then(({success, channelName, shortChannelId, channelClaimId, message}) => {\n        if (success) {\n          this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);\n        } else {\n          this.setState({'error': message});\n        };\n      })\n      .catch(error => {\n        if (error.message) {\n          this.setState({'error': error.message});\n        } else {\n          this.setState({'error': error});\n        }\n      });\n  }\n  render () {\n    return (\n      <form className=\"form-group\" onSubmit={this.loginToChannel}>\n        <ChannelLoginNameInput\n          channelName={this.state.channelName}\n          handleInput={this.handleInput}\n        />\n        <ChannelLoginPasswordInput\n          channelPassword={this.state.channelPassword}\n          handleInput={this.handleInput}\n        />\n        <FormFeedbackDisplay errorMessage={this.state.error} />\n        <ButtonPrimary\n          type={'submit'}\n          value={'Authenticate'}\n          onClickHandler={this.loginToChannel}\n        />\n      </form>\n    );\n  }\n}\n\nexport default ChannelLoginForm;\n"
  },
  {
    "path": "client/src/containers/ChannelSelect/index.js",
    "content": "import {connect} from 'react-redux';\nimport {setPublishInChannel, updateSelectedChannel, updateError} from '../../actions/publish';\n// import isApprovedChannel from '../../../../utils/isApprovedChannel';\nimport View from './view';\n\nconst mapStateToProps = ({ publish, site, channel: { loggedInChannel: { name, shortId, longId } } }) => {\n  return {\n    // isApprovedChannel  : isApprovedChannel({ longId }, site.approvedChannels),\n    publishOnlyApproved: site.publishOnlyApproved,\n    // closedRegistration : site.closedRegistration,\n    loggedInChannelName: name,\n    publishInChannel   : publish.publishInChannel,\n    selectedChannel    : publish.selectedChannel,\n    channelError       : publish.error.channel,\n    longId,\n  };\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onPublishInChannelChange: (value) => {\n      dispatch(updateError('channel', null));\n      dispatch(setPublishInChannel(value));\n    },\n    onChannelSelect: (value) => {\n      dispatch(updateError('channel', null));\n      dispatch(updateSelectedChannel(value));\n    },\n  };\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/ChannelSelect/view.jsx",
    "content": "import React from 'react';\nimport ChannelLoginForm from '@containers/ChannelLoginForm';\nimport ChannelCreateForm from '@containers/ChannelCreateForm';\nimport { LOGIN, CREATE } from '../../constants/publish_channel_select_states';\nimport RowLabeled from '@components/RowLabeled';\nimport ChooseAnonymousPublishRadio from '@components/ChooseAnonymousPublishRadio';\nimport ChooseChannelPublishRadio from '@components/ChooseChannelPublishRadio';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\nimport Label from '@components/Label';\nimport ChannelSelectDropdown from '@components/ChannelSelectDropdown';\n\nclass ChannelSelect extends React.Component {\n  constructor (props) {\n    super(props);\n    this.toggleAnonymousPublish = this.toggleAnonymousPublish.bind(this);\n    this.handleSelection = this.handleSelection.bind(this);\n  }\n  componentWillMount () {\n    const { loggedInChannelName, onChannelSelect, publishOnlyApproved, onPublishInChannelChange } = this.props;\n    if (loggedInChannelName) {\n      onChannelSelect(loggedInChannelName);\n    }\n    if (publishOnlyApproved) {\n      onPublishInChannelChange(true);\n    }\n  }\n  toggleAnonymousPublish (event) {\n    const value = event.target.value;\n    if (value === 'anonymous') {\n      this.props.onPublishInChannelChange(false);\n    } else {\n      this.props.onPublishInChannelChange(true);\n    }\n  }\n  handleSelection (event) {\n    const selectedOption = event.target.selectedOptions[0].value;\n    this.props.onChannelSelect(selectedOption);\n  }\n  render () {\n    const { publishInChannel, channelError, selectedChannel, loggedInChannelName, publishOnlyApproved } = this.props;\n    if (publishOnlyApproved) {\n      return (\n        <React.Fragment>\n          <RowLabeled\n            label={<Label value={'Channel:'} />}\n            content={<span>{loggedInChannelName}</span>}\n          />\n        </React.Fragment>\n      );\n    }\n    return (\n      <React.Fragment>\n        <RowLabeled\n          label={\n            <ChooseAnonymousPublishRadio\n              publishInChannel={publishInChannel}\n              toggleAnonymousPublish={this.toggleAnonymousPublish}\n            />\n          }\n          content={\n            <ChooseChannelPublishRadio\n              publishInChannel={publishInChannel}\n              toggleAnonymousPublish={this.toggleAnonymousPublish}\n            />\n          }\n        />\n        <FormFeedbackDisplay\n          errorMessage={channelError}\n          defaultMessage={'Publish anonymously or in a channel'}\n        />\n\n        { this.props.publishInChannel && (\n          <div>\n            <RowLabeled\n              label={\n                <Label value={'Channel:'} />\n              }\n              content={\n                <ChannelSelectDropdown\n                  selectedChannel={selectedChannel}\n                  handleSelection={this.handleSelection}\n                  loggedInChannelName={loggedInChannelName}\n                />\n              }\n            />\n            { (selectedChannel === LOGIN) && <ChannelLoginForm /> }\n            { (selectedChannel === CREATE) && <ChannelCreateForm /> }\n          </div>\n        )}\n      </React.Fragment>\n    );\n  }\n}\n\nexport default ChannelSelect;\n"
  },
  {
    "path": "client/src/containers/ChannelTools/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ site: { closedRegistration } }) => {\n  return {\n    closedRegistration,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/ChannelTools/view.jsx",
    "content": "import React from 'react';\nimport ChannelLoginForm from '@containers/ChannelLoginForm';\nimport ChannelCreateForm from '@containers/ChannelCreateForm';\nimport Row from '@components/Row';\n\nclass ChannelTools extends React.Component {\n  render () {\n    return (\n      <div>\n        <h3 className=\"form-title\">Log in to existing channel</h3>\n        <ChannelLoginForm />\n        {!this.props.closedRegistration && (\n          <React.Fragment>\n            <h3 className=\"form-title\">Create new channel</h3>\n            <ChannelCreateForm />\n          </React.Fragment>\n        )}\n      </div>\n    );\n  }\n}\n\nexport default ChannelTools;\n"
  },
  {
    "path": "client/src/containers/Dropzone/index.js",
    "content": "import { connect } from 'react-redux';\nimport { selectFile, updateError, clearFile } from '../../actions/publish';\nimport { selectAsset } from '../../selectors/show';\nimport View from './view';\nimport siteConfig from '@config/siteConfig.json';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\n\nconst {\n  assetDefaults: { thumbnail: defaultThumbnail },\n} = siteConfig;\n\nconst mapStateToProps = ({ show, publish: { file, thumbnail, error, isUpdate } }) => {\n  const fileError = error.file;\n  const obj = { file, thumbnail, fileError, isUpdate };\n  let asset, name, claimId, fileExt, outpoint, sourceUrl;\n  if (isUpdate) {\n    asset = selectAsset(show);\n    const { claimData } = asset;\n    if (asset) {\n      obj.fileExt = claimData.contentType.split('/')[1];\n      if (obj.fileExt === 'mp4') {\n        obj.sourceUrl = claimData.thumbnail ? claimData.thumbnail : defaultThumbnail;\n      } else {\n        ({ fileExt, outpoint } = claimData);\n        obj.sourceUrl = `${createCanonicalLink({ asset: claimData })}.${fileExt}?${outpoint}`;\n      }\n    }\n  }\n  return obj;\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    selectFile: file => {\n      dispatch(selectFile(file));\n    },\n    setFileError: value => {\n      dispatch(clearFile());\n      dispatch(updateError('file', value));\n    },\n  };\n};\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(View);\n"
  },
  {
    "path": "client/src/containers/Dropzone/view.jsx",
    "content": "import React from 'react';\n\nimport Memeify from '@components/Memeify';\nimport DropzonePreviewImage from '@components/DropzonePreviewImage';\nimport DropzoneDropItDisplay from '@components/DropzoneDropItDisplay';\nimport DropzoneInstructionsDisplay from '@components/DropzoneInstructionsDisplay';\nimport validateFileForPublish from '@globalutils/validateFileForPublish';\n\nimport { library } from '@fortawesome/fontawesome-svg-core';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faEdit } from '@fortawesome/free-solid-svg-icons';\n\nconst isFacebook = (() => {\n  if(typeof window === 'undefined' || typeof window.navigator.userAgent === 'undefined') {\n    return false;\n  }\n  return window.navigator.userAgent.indexOf('FBAN') !== -1 || window.navigator.userAgent.indexOf('FBAV') !== -1;\n})();\n\nclass Dropzone extends React.Component {\n  constructor (props) {\n    super(props);\n\n    this.state = {\n      dragOver   : false,\n      mouseOver  : false,\n      dimPreview : false,\n      filePreview: null,\n      memeify    : false,\n    };\n\n    if (props.file) {\n      // No side effects allowed with `getDerivedStateFromProps`, so\n      // we must use `componentDidUpdate` and `constructor` routines.\n      // Note: `FileReader` has an `onloadend` side-effect\n      this.updateFilePreview();\n    }\n\n    this.handleDrop = this.handleDrop.bind(this);\n    this.handleDragOver = this.handleDragOver.bind(this);\n    this.handleDragEnd = this.handleDragEnd.bind(this);\n    this.handleDragEnter = this.handleDragEnter.bind(this);\n    this.handleDragLeave = this.handleDragLeave.bind(this);\n    this.handleMouseEnter = this.handleMouseEnter.bind(this);\n    this.handleMouseLeave = this.handleMouseLeave.bind(this);\n    this.handleClick = this.handleClick.bind(this);\n    this.handleFileInput = this.handleFileInput.bind(this);\n    this.chooseFile = this.chooseFile.bind(this);\n\n    this.fileInput = React.createRef();\n  }\n\n  componentDidMount() {\n    if(isFacebook) {\n      // See https://github.com/lbryio/spee.ch/issues/782\n      this.fileInput.current.removeAttribute('accept');\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    if(prevProps.file !== this.props.file) {\n      this.updateFilePreview();\n    }\n  }\n\n  updateFilePreview() {\n    if (this.props.file) {\n      const fileReader = new FileReader();\n      fileReader.readAsDataURL(this.props.file);\n      fileReader.onloadend = () => {\n        this.setState({\n          filePreview: fileReader.result\n        });\n      };\n    }\n  }\n\n  handleDrop (event) {\n    event.preventDefault();\n    this.setState({dragOver: false});\n    // if dropped items aren't files, reject them\n    const dt = event.dataTransfer;\n    if (dt.items) {\n      if (dt.items[0].kind === 'file') {\n        const droppedFile = dt.items[0].getAsFile();\n        this.chooseFile(droppedFile);\n      }\n    }\n  }\n\n  handleDragOver (event) {\n    event.preventDefault();\n  }\n\n  handleDragEnd (event) {\n    var dt = event.dataTransfer;\n    if (dt.items) {\n      for (var i = 0; i < dt.items.length; i++) {\n        dt.items.remove(i);\n      }\n    } else {\n      event.dataTransfer.clearData();\n    }\n  }\n\n  handleDragEnter () {\n    this.setState({dragOver: true, dimPreview: true});\n  }\n\n  handleDragLeave () {\n    this.setState({dragOver: false, dimPreview: false});\n  }\n\n  handleMouseEnter () {\n    this.setState({mouseOver: true, dimPreview: true});\n  }\n\n  handleMouseLeave () {\n    this.setState({mouseOver: false, dimPreview: false});\n  }\n\n  handleClick (event) {\n    event.preventDefault();\n    document.getElementById('file_input').click();\n  }\n\n  handleFileInput (event) {\n    event.preventDefault();\n    const fileList = event.target.files;\n    this.chooseFile(fileList[0]);\n  }\n\n  chooseFile (file) {\n    if (file) {\n      try {\n        validateFileForPublish(file); // validate the file's name, type, and size\n      } catch (error) {\n        return this.props.setFileError(error.message);\n      }\n      // stage it so it will be ready when the publish button is clicked\n      this.props.selectFile(file);\n    }\n  }\n\n  selectFileFromCanvas (canvas) {\n    const destinationFormat = 'image/jpeg';\n\n    canvas.toBlob((blob) => {\n      const file = new File([blob], `memeify-${Math.random().toString(36).substring(7)}.jpg`, {\n        type: destinationFormat,\n      });\n\n      this.props.selectFile(file);\n\n      // TODO: Add ability to reset.\n      this.setState({\n        memeify: false,\n      });\n    }, destinationFormat, 0.95);\n  }\n\n  render () {\n    const { dragOver, mouseOver, dimPreview, filePreview, memeify } = this.state;\n    const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;\n\n    const hasContent = !!(file || isUpdate);\n\n    const dropZoneHooks = file ? {} : {\n      onDrop: this.handleDrop,\n      onDragOver: this.handleDragOver,\n      onDragEnd: this.handleDragEnd,\n      onDragEnter: this.handleDragEnter,\n      onDragLeave: this.handleDragLeave,\n      onMouseEnter: this.handleMouseEnter,\n      onMouseLeave: this.handleMouseLeave,\n      onClick: this.handleClick,\n    };\n\n    const dropZonePreviewProps = file ? {\n      dimPreview,\n      file,\n      thumbnail,\n    } : {\n      dimPreview: true,\n      isUpdate: true,\n      sourceUrl,\n    };\n\n    const memeifyContent = memeify && file && filePreview ? (\n      <Memeify flex toolbarClassName={'dropzone-memeify-toolbar'} onSave={(canvas) => this.selectFileFromCanvas(canvas)}>\n        <div style={{ background: '#fff', flex: 1, pointerEvents: 'none' }}>\n          <img style={{ width: '100%' }} src={filePreview} />\n        </div>\n      </Memeify>\n    ) : null;\n\n    const dropZoneClassName = 'dropzone' + (dragOver ? ' dropzone--drag-over' : '');\n\n    return (\n      <React.Fragment>\n        {isUpdate && fileExt === 'mp4' ? (\n          <p>Video updates are currently disabled. This feature will be available soon. You can edit metadata.</p>\n        ) : (\n          <div className={'dropzone-wrapper'}>\n            { hasContent && !memeify && fileExt !== 'mp4' && (\n              <div className={'dropzone-memeify-button'} onClick={() => this.setState({ memeify: !memeify })}>\n                <FontAwesomeIcon icon={faEdit} /> Memeify\n              </div>\n            )}\n            <form>\n              <input\n                ref={this.fileInput}\n                className='input-file'\n                type='file'\n                id='file_input'\n                name='file_input'\n                accept='video/*,image/*'\n                onChange={this.handleFileInput}\n                encType='multipart/form-data'\n              />\n            </form>\n            <div className={dropZoneClassName} {...dropZoneHooks}>\n              {hasContent ? (\n                <div className={'dropzone-preview-wrapper' + (memeifyContent ? ' dropzone-preview-memeify' : '')}>\n                  <DropzonePreviewImage {...dropZonePreviewProps} />\n                  <div className={'dropzone-preview-overlay'}>\n                    { dragOver ? <DropzoneDropItDisplay /> : null }\n                    { mouseOver ? (\n                      <DropzoneInstructionsDisplay\n                        fileError={fileError}\n                        message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}\n                      />\n                    ) : null }\n                    {memeifyContent}\n                  </div>\n                </div>\n              ) : (\n                dragOver ? <DropzoneDropItDisplay /> : (\n                  <DropzoneInstructionsDisplay fileError={fileError} />\n                )\n              )}\n              {memeifyContent ? <div className={'dropzone-memeify-saveMessage'}>{`Don't forget to save before you publish.`}</div> : null}\n            </div>\n          </div>\n        )}\n      </React.Fragment>\n    );\n  }\n};\n\nexport default Dropzone;\n"
  },
  {
    "path": "client/src/containers/NavigationLinks/index.jsx",
    "content": "import { connect } from 'react-redux';\nimport { logOutChannel, checkForLoggedInChannel } from '../../actions/channel';\nimport isApprovedChannel from '../../../../utils/isApprovedChannel';\nimport View from './view';\n\nconst mapStateToProps = ({ site, channel: { loggedInChannel: { name, shortId, longId } } }) => {\n  return {\n    showPublish       : (!site.publishOnlyApproved || isApprovedChannel({ longId }, site.approvedChannels)),\n    closedRegistration: site.closedRegistration,\n    channelName       : name,\n    channelShortId    : shortId,\n    channelLongId     : longId,\n  };\n};\n\nconst mapDispatchToProps = {\n  checkForLoggedInChannel,\n  logOutChannel,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/NavigationLinks/view.jsx",
    "content": "import React from 'react';\nimport { NavLink, withRouter } from 'react-router-dom';\nimport NavBarChannelOptionsDropdown from '@components/NavBarChannelOptionsDropdown';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\n\nconst VIEW = 'VIEW';\nconst LOGOUT = 'LOGOUT';\n\nclass NavigationLinks extends React.Component {\n  constructor (props) {\n    super(props);\n    this.handleSelection = this.handleSelection.bind(this);\n  }\n  componentDidMount () {\n    this.props.checkForLoggedInChannel();\n  }\n  handleSelection (event) {\n    const { history, channelName: name, channelShortId: shortId } = this.props;\n    const value = event.target.selectedOptions[0].value;\n    switch (value) {\n      case LOGOUT:\n        this.props.logOutChannel();\n        break;\n      case VIEW:\n        // redirect to channel page\n        history.push(createCanonicalLink({ channel: { name, shortId } }));\n        break;\n      default:\n        break;\n    }\n  }\n  render () {\n    const { channelName, showPublish, closedRegistration } = this.props;\n    return (\n      <div className='navigation-links'>\n        {/*{showPublish && <NavLink*/}\n        {/*  className='nav-bar-link link--nav'*/}\n        {/*  activeClassName='link--nav-active'*/}\n        {/*  to='/'*/}\n        {/*  exact*/}\n        {/*>*/}\n        {/*  Publish*/}\n        {/*</NavLink>}*/}\n        <NavLink\n          className='nav-bar-link link--nav'\n          activeClassName='link--nav-active'\n          to='/about'\n        >\n          About\n        </NavLink>\n        {/*{ channelName ? (*/}\n        {/*  <NavBarChannelOptionsDropdown*/}\n        {/*    channelName={this.props.channelName}*/}\n        {/*    handleSelection={this.handleSelection}*/}\n        {/*    defaultSelection={this.props.channelName}*/}\n        {/*    VIEW={VIEW}*/}\n        {/*    LOGOUT={LOGOUT}*/}\n        {/*  />*/}\n        {/*) : !closedRegistration && (*/}\n        {/*  <NavLink*/}\n        {/*    id='nav-bar-login-link'*/}\n        {/*    className='nav-bar-link link--nav'*/}\n        {/*    activeClassName='link--nav-active'*/}\n        {/*    to='/login'*/}\n        {/*  >*/}\n        {/*    Channel*/}\n        {/*  </NavLink>*/}\n        {/*)}*/}\n      </div>\n    );\n  }\n}\n\nexport default withRouter(NavigationLinks);\n"
  },
  {
    "path": "client/src/containers/PublishDetails/index.js",
    "content": "import { connect } from 'react-redux';\nimport { clearFile, startPublish, abandonClaim } from '../../actions/publish';\nimport { selectAsset } from '../../selectors/show';\nimport View from './view';\n\nconst mapStateToProps = ({ show, publish }) => {\n  return {\n    file      : publish.file,\n    isUpdate  : publish.isUpdate,\n    hasChanged: publish.hasChanged,\n    asset     : selectAsset(show),\n  };\n};\n\nconst mapDispatchToProps = {\n  clearFile,\n  startPublish,\n  abandonClaim,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/PublishDetails/view.jsx",
    "content": "import React from 'react';\nimport {Link, withRouter} from 'react-router-dom';\nimport PublishUrlInput from '@containers/PublishUrlInput';\nimport PublishThumbnailInput from '@containers/PublishThumbnailInput';\nimport PublishMetadataInputs from '@containers/PublishMetadataInputs';\nimport ChannelSelect from '@containers/ChannelSelect';\nimport Row from '@components/Row';\nimport Label from '@components/Label';\nimport RowLabeled from '@components/RowLabeled';\nimport ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';\nimport ButtonSecondary from '@components/ButtonSecondary';\nimport SpaceAround from '@components/SpaceAround';\nimport PublishFinePrint from '@components/PublishFinePrint';\nimport { SAVE } from '../../constants/confirmation_messages';\n\nclass PublishDetails extends React.Component {\n  constructor (props) {\n    super(props);\n    this.onPublishSubmit = this.onPublishSubmit.bind(this);\n    this.abandonClaim = this.abandonClaim.bind(this);\n    this.onCancel = this.onCancel.bind(this);\n  }\n  onPublishSubmit () {\n    this.props.startPublish(this.props.history);\n  }\n  abandonClaim () {\n    const {asset, history} = this.props;\n    if (asset) {\n      const {claimData} = asset;\n      this.props.abandonClaim({claimData, history});\n    }\n  }\n  onCancel () {\n    const { isUpdate, hasChanged, clearFile, history } = this.props;\n    if (isUpdate || !hasChanged) {\n      history.push('/');\n    } else {\n      if (confirm(SAVE)) {\n        clearFile();\n      }\n    }\n  }\n  render () {\n    const {file, isUpdate, asset} = this.props;\n    return (\n      <div>\n        {isUpdate ? (asset && (\n          <React.Fragment>\n            <RowLabeled\n              label={\n                <Label value={'Channel:'} />\n              }\n              content={\n                <span className='text'>\n                  {asset.claimData.channelName}\n                </span>\n              }\n            />\n          </React.Fragment>\n        )) : (\n          <React.Fragment>\n            <Row>\n              <PublishUrlInput />\n            </Row>\n\n            <ChannelSelect />\n          </React.Fragment>\n        )}\n\n        { file && file.type === 'video/mp4' && (\n          <Row>\n            <PublishThumbnailInput />\n          </Row>\n        )}\n\n        <Row>\n          <PublishMetadataInputs />\n        </Row>\n\n        <Row>\n          <ButtonPrimaryJumbo\n            value={isUpdate ? 'Update' : 'Publish'}\n            onClickHandler={this.onPublishSubmit}\n          />\n        </Row>\n\n        {isUpdate && (\n          <Row>\n            <SpaceAround>\n              <ButtonSecondary\n                value={'Abandon Claim'}\n                onClickHandler={this.abandonClaim}\n              />\n            </SpaceAround>\n          </Row>\n        )}\n\n        <Row>\n          <SpaceAround>\n            <ButtonSecondary\n              value={'Cancel'}\n              onClickHandler={this.onCancel}\n            />\n          </SpaceAround>\n        </Row>\n\n        <Row>\n          <PublishFinePrint />\n        </Row>\n      </div>\n    );\n  }\n};\n\nexport default withRouter(PublishDetails);\n"
  },
  {
    "path": "client/src/containers/PublishDisabledMessage/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ publish }) => {\n  return {\n    message: publish.disabledMessage,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/PublishDisabledMessage/view.jsx",
    "content": "import React from 'react';\n\nclass PublishDisabledMessage extends React.Component {\n  render () {\n    const message = this.props.message;\n    return (\n      <div className={'publish-disabled-message'}>\n        <div className={'message'}>\n          <p className={'text--secondary'}>Publishing is currently disabled.</p>\n          <p className={'text--secondary'}>\n            Try <a className='link--primary' href='https://lbry.tv' target='_blank'>lbry.tv</a>\n          </p>\n          <p className={'text--secondary'}>{message}</p>\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default PublishDisabledMessage;\n"
  },
  {
    "path": "client/src/containers/PublishMetadataInputs/index.js",
    "content": "import { connect } from 'react-redux';\nimport { updateMetadata, toggleMetadataInputs } from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ publish }) => {\n  return {\n    showMetadataInputs: publish.showMetadataInputs,\n    description: publish.metadata.description,\n    license: publish.metadata.license,\n    licenseUrl: publish.metadata.licenseUrl,\n    nsfw: publish.metadata.nsfw,\n    isUpdate: publish.isUpdate,\n  };\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onMetadataChange: (name, value) => {\n      dispatch(updateMetadata(name, value));\n    },\n    onToggleMetadataInputs: value => {\n      dispatch(toggleMetadataInputs(value));\n    },\n  };\n};\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(View);\n"
  },
  {
    "path": "client/src/containers/PublishMetadataInputs/view.jsx",
    "content": "import React from 'react';\nimport PublishDescriptionInput from '@components/PublishDescriptionInput';\nimport PublishLicenseInput from '@components/PublishLicenseInput';\nimport PublishLicenseUrlInput from '@components/PublishLicenseUrlInput';\nimport PublishNsfwInput from '@components/PublishNsfwInput';\nimport ButtonSecondary from '@components/ButtonSecondary';\n\nclass PublishMetadataInputs extends React.Component {\n  constructor (props) {\n    super(props);\n    this.toggleShowInputs = this.toggleShowInputs.bind(this);\n    this.handleInput = this.handleInput.bind(this);\n    this.handleSelect = this.handleSelect.bind(this);\n  }\n  toggleShowInputs () {\n    this.props.onToggleMetadataInputs(!this.props.showMetadataInputs);\n  }\n  handleInput (event) {\n    const target = event.target;\n    const value = target.type === 'checkbox' ? target.checked : target.value;\n    const name = target.name;\n    this.props.onMetadataChange(name, value);\n  }\n  handleSelect (event) {\n    const name = event.target.name;\n    const selectedOption = event.target.selectedOptions[0].value;\n    this.props.onMetadataChange(name, selectedOption);\n    if (name === 'license' && selectedOption !== 'Creative Commons'){\n      this.props.onMetadataChange('licenseUrl', '');\n    }\n  }\n  render () {\n    const { showMetadataInputs, description, isUpdate, nsfw, license, licenseUrl } = this.props;\n    return (\n      <div>\n        {(showMetadataInputs || isUpdate) && (\n          <React.Fragment>\n            <PublishDescriptionInput\n              description={description}\n              handleInput={this.handleInput}\n            />\n            <PublishLicenseInput\n              handleSelect={this.handleSelect}\n              license={license}\n            />\n            { (this.props.license === 'Creative Commons') && (\n              <PublishLicenseUrlInput\n                handleSelect={this.handleSelect}\n                licenseUrl={licenseUrl}\n              />\n            )}\n            <PublishNsfwInput\n              nsfw={nsfw}\n              handleInput={this.handleInput}\n            />\n          </React.Fragment>\n        )}\n        {!isUpdate && (\n          <ButtonSecondary\n            value={showMetadataInputs ? 'less' : 'more'}\n            onClickHandler={this.toggleShowInputs}\n          />\n        )}\n      </div>\n    );\n  }\n}\n\nexport default PublishMetadataInputs;\n"
  },
  {
    "path": "client/src/containers/PublishStatus/index.js",
    "content": "import {connect} from 'react-redux';\nimport {clearFile} from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ publish }) => {\n  return {\n    status : publish.status.status,\n    message: publish.status.message,\n  };\n};\n\nconst mapDispatchToProps = {\n  clearFile,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/PublishStatus/view.jsx",
    "content": "import React from 'react';\nimport ProgressBar from '@components/ProgressBar';\nimport * as publishStates from '../../constants/publish_claim_states';\nimport ButtonSecondary from '@components/ButtonSecondary';\nimport Row from '@components/Row';\n\nclass PublishStatus extends React.Component {\n  render () {\n    const { status, message, clearFile } = this.props;\n    return (\n      <div className={'publish-status'}>\n        {status === publishStates.LOAD_START &&\n          <div className={'status'}>\n            <Row>\n              <p>File is loading to server</p>\n            </Row>\n            <Row>\n              <p className={'text--secondary'}>0%</p>\n            </Row>\n          </div>\n        }\n        {status === publishStates.LOADING &&\n          <div className={'status'}>\n            <Row>\n              <p>File is loading to server</p>\n            </Row>\n            <Row>\n              <p className={'text--secondary'}>{message}</p>\n            </Row>\n          </div>\n        }\n        {status === publishStates.PUBLISHING &&\n          <div className={'status'}>\n            <Row>\n              <p>Upload complete.  Your file is now being published on the blockchain...</p>\n            </Row>\n            <Row>\n              <ProgressBar size={12} />\n            </Row>\n            <Row>\n              <p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.com/faq/what-is-lbry'>Learn more.</a></p>\n            </Row>\n          </div>\n        }\n        {status === publishStates.SUCCEEDED &&\n          <div className={'status'}>\n            <Row>\n              <p>Your publish is complete! You are being redirected to it now.</p>\n            </Row>\n            <Row>\n              <p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>\n            </Row>\n          </div>\n        }\n        {status === publishStates.FAILED &&\n          <div className={'status'}>\n            <Row>\n              <p>Something went wrong...</p>\n            </Row>\n            <Row>\n              <p className={'text--strong'}>{message}</p>\n            </Row>\n            <Row>\n              <p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://chat.lbry.com' target='_blank'>lbry discord</a></p>\n            </Row>\n            <Row>\n              <ButtonSecondary\n                value={'Reset'}\n                onClickHandler={clearFile}\n              />\n            </Row>\n          </div>\n        }\n        {status === publishStates.ABANDONING &&\n          <div className={'status'}>\n            <Row>\n              <p>Your claim is being abandoned.</p>\n            </Row>\n          </div>\n        }\n      </div>\n    );\n  }\n};\n\nexport default PublishStatus;\n"
  },
  {
    "path": "client/src/containers/PublishThumbnailInput/index.js",
    "content": "import { connect } from 'react-redux';\nimport { onNewThumbnail } from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ publish: { file } }) => {\n  return {\n    file,\n  };\n};\n\nconst mapDispatchToProps = {\n  onNewThumbnail,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/PublishThumbnailInput/view.jsx",
    "content": "import React from 'react';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\nimport SpaceBetween from '@components/SpaceBetween';\n\nfunction dataURItoBlob (dataURI) {\n  // convert base64/URLEncoded data component to raw binary data held in a string\n  let byteString = atob(dataURI.split(',')[1]);\n  // separate out the mime component\n  let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];\n  // write the bytes of the string to a typed array\n  let ia = new Uint8Array(byteString.length);\n  for (let i = 0; i < byteString.length; i++) {\n    ia[i] = byteString.charCodeAt(i);\n  }\n  return new Blob([ia], {type: mimeString});\n}\n\nclass PublishThumbnailInput extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      videoSource   : null,\n      error         : null,\n      sliderMinRange: 1,\n      sliderMaxRange: null,\n      sliderValue   : null,\n    };\n    this.handleVideoLoadedData = this.handleVideoLoadedData.bind(this);\n    this.handleSliderChange = this.handleSliderChange.bind(this);\n    this.createThumbnail = this.createThumbnail.bind(this);\n  }\n  componentDidMount () {\n    const { file } = this.props;\n    this.setVideoSource(file);\n  }\n  componentWillReceiveProps (nextProps) {\n    // if file changes\n    if (nextProps.file && nextProps.file !== this.props.file) {\n      const { file } = nextProps;\n      this.setVideoSource(file);\n    };\n  }\n  setVideoSource (file) {\n    const previewReader = new FileReader();\n    previewReader.readAsDataURL(file);\n    previewReader.onloadend = () => {\n      const dataUri = previewReader.result;\n      const blob = dataURItoBlob(dataUri);\n      const videoSource = URL.createObjectURL(blob);\n      this.setState({ videoSource });\n    };\n  }\n  handleVideoLoadedData (event) {\n    const duration = event.target.duration;\n    const totalMinutes = Math.floor(duration / 60);\n    const totalSeconds = Math.floor(duration % 60);\n    // set the slider\n    this.setState({\n      sliderMaxRange: duration * 100,\n      sliderValue   : duration * 100 / 2,\n      totalMinutes,\n      totalSeconds,\n    });\n    // update the current time of the video\n    let video = document.getElementById('video-thumb-player');\n    video.currentTime = duration / 2;\n  }\n  handleSliderChange (event) {\n    const value = parseInt(event.target.value);\n    // update the slider value\n    this.setState({\n      sliderValue: value,\n    });\n    // update the current time of the video\n    let video = document.getElementById('video-thumb-player');\n    video.currentTime = value / 100;\n  }\n  createThumbnail () {\n    // take a snapshot\n    let video = document.getElementById('video-thumb-player');\n    let canvas = document.createElement('canvas');\n    canvas.width = video.videoWidth;\n    canvas.height = video.videoHeight;\n    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);\n    const dataUrl = canvas.toDataURL();\n    const blob = dataURItoBlob(dataUrl);\n    const snapshot = new File([blob], `thumbnail.png`, {\n      type: 'image/png',\n    });\n    // set the thumbnail in redux store\n    if (snapshot) {\n      this.props.onNewThumbnail(snapshot);\n    }\n  }\n  render () {\n    const { error, videoSource, sliderMinRange, sliderMaxRange, sliderValue, totalMinutes, totalSeconds } = this.state;\n    return (\n      <div>\n        <label className='label'>Thumbnail:</label>\n        <video\n          id='video-thumb-player'\n          preload='metadata'\n          muted\n          style={{display: 'none'}}\n          playsInline\n          onLoadedData={this.handleVideoLoadedData}\n          src={videoSource}\n          onSeeked={this.createThumbnail}\n        />\n        {\n          sliderValue ? (\n            <div>\n              <SpaceBetween style={{width: '100%'}}>\n                <span className='text--small text--secondary'>0'00\"</span>\n                <span className='text--small text--secondary'>{totalMinutes}'{totalSeconds}\"</span>\n              </SpaceBetween>\n              <div>\n                <input\n                  type='range'\n                  min={sliderMinRange}\n                  max={sliderMaxRange}\n                  value={sliderValue}\n                  className='input-slider'\n                  onChange={this.handleSliderChange}\n                />\n              </div>\n            </div>\n          ) : (\n            <span className={'text--small text--secondary'}>loading... </span>\n          )\n        }\n        <FormFeedbackDisplay\n          errorMessage={error}\n          defaultMessage={'Use slider to set thumbnail'}\n        />\n      </div>\n    );\n  }\n}\n\nexport default PublishThumbnailInput;\n"
  },
  {
    "path": "client/src/containers/PublishTitleInput/index.js",
    "content": "import { connect } from 'react-redux';\nimport { updateMetadata } from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ publish }) => {\n  return {\n    title: publish.metadata.title,\n  };\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onMetadataChange: (name, value) => {\n      dispatch(updateMetadata(name, value));\n    },\n  };\n};\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(View);\n"
  },
  {
    "path": "client/src/containers/PublishTitleInput/view.jsx",
    "content": "import React from 'react';\n\nclass PublishTitleInput extends React.Component {\n  constructor (props) {\n    super(props);\n    this.handleInput = this.handleInput.bind(this);\n  }\n  handleInput (e) {\n    const name = e.target.name;\n    const value = e.target.value;\n    this.props.onMetadataChange(name, value);\n  }\n  render () {\n    return (\n      <input\n        type='text'\n        id='publish-title'\n        className={'text--extra-large input--full-width'}\n        name='title'\n        placeholder='Give your content a title...'\n        onChange={this.handleInput}\n        value={this.props.title} />\n    );\n  }\n}\n\nexport default PublishTitleInput;\n"
  },
  {
    "path": "client/src/containers/PublishTool/index.js",
    "content": "import {connect} from 'react-redux';\nimport View from './view';\nimport {selectAsset} from '../../selectors/show';\nimport {createPermanentURI} from '@clientutils/createPermanentURI';\n\nconst mapStateToProps = props => {\n  const { show, publish } = props;\n  const asset = selectAsset(show);\n  let uri;\n  if (asset) {\n    uri = `lbry://${createPermanentURI(asset)}`;\n  }\n  return {\n    disabled  : publish.disabled,\n    file      : publish.file,\n    status    : publish.status.status,\n    isUpdate  : publish.isUpdate,\n    hasChanged: publish.hasChanged,\n    uri,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/PublishTool/view.jsx",
    "content": "import React from 'react';\nimport { withRouter, Prompt } from 'react-router';\nimport Dropzone from '@containers/Dropzone';\nimport PublishPreview from '@components/PublishPreview';\nimport PublishStatus from '@containers/PublishStatus';\nimport PublishDisabledMessage from '@containers/PublishDisabledMessage';\nimport { SAVE } from '../../constants/confirmation_messages';\n\nclass PublishTool extends React.Component {\n  render () {\n    const {disabled, file, isUpdate, hasChanged, uri, status, location: currentLocation} = this.props;\n    if (disabled) {\n      return (\n        <PublishDisabledMessage />\n      );\n    } else {\n      if (file || isUpdate) {\n        if (status) {\n          return (\n            <PublishStatus />\n          );\n        } else {\n          return (\n            <React.Fragment>\n              <Prompt\n                when={hasChanged}\n                message={(location) => location.pathname === currentLocation.pathname ? false : SAVE}\n              />\n              <PublishPreview isUpdate={isUpdate} uri={uri} />\n            </React.Fragment>\n          );\n        }\n      }\n      return <Dropzone />;\n    }\n  }\n};\n\nexport default withRouter(PublishTool);\n"
  },
  {
    "path": "client/src/containers/PublishUrlInput/index.js",
    "content": "import { connect } from 'react-redux';\nimport { updateClaim, updateError, validateClaim } from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ channel, publish }) => {\n  return {\n    loggedInChannelName   : channel.loggedInChannel.name,\n    loggedInChannelShortId: channel.loggedInChannel.shortId,\n    fileName              : publish.file.name,\n    publishInChannel      : publish.publishInChannel,\n    selectedChannel       : publish.selectedChannel,\n    claim                 : publish.claim,\n    urlError              : publish.error.url,\n  };\n};\n\nconst mapDispatchToProps = {\n  validateClaim,\n  updateClaim,\n  updateError,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/containers/PublishUrlInput/view.jsx",
    "content": "import React from 'react';\nimport UrlMiddle from '@components/PublishUrlMiddleDisplay';\nimport FormFeedbackDisplay from '@components/FormFeedbackDisplay';\n\nclass PublishUrlInput extends React.Component {\n  constructor (props) {\n    super(props);\n    this.handleInput = this.handleInput.bind(this);\n  }\n  cleanseInput (input) {\n    input = input.replace(/\\s+/g, '-');\n    input = input.replace(/[^A-Za-z0-9-]/g, '');\n    return input;\n  }\n  componentDidMount () {\n    const { claim, fileName } = this.props;\n    if (!claim) {\n      this.setInitialClaimName(fileName);\n    }\n  }\n  componentWillReceiveProps ({ claim, fileName }) {\n    // if a new file was chosen, update the claim name\n    if (fileName !== this.props.fileName) {\n      return this.setInitialClaimName(fileName);\n    }\n  }\n  setInitialClaimName (fileName) {\n    const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));\n    const cleanFileName = this.cleanseInput(fileNameWithoutEnding);\n    this.updateAndValidateClaimInput(cleanFileName);\n  }\n  handleInput (event) {\n    let value = event.target.value;\n    value = this.cleanseInput(value);\n    this.updateAndValidateClaimInput(value);\n  }\n  updateAndValidateClaimInput (value) {\n    if (value) {\n      this.props.validateClaim(value);\n    } else {\n      this.props.updateError('url', 'Choose a custom url');\n    }\n    this.props.updateClaim(value);\n  }\n  render () {\n    const { claim, loggedInChannelName, loggedInChannelShortId, publishInChannel, selectedChannel, urlError } = this.props;\n    return (\n      <div>\n        <div className={'publish-url-input'}>\n          <div className={'align-left'}>\n            <span className='publish-url-text'>spee.ch&nbsp;/&nbsp;</span>\n          </div>\n          <div className={'shrink'}>\n            <UrlMiddle\n              publishInChannel={publishInChannel}\n              selectedChannel={selectedChannel}\n              loggedInChannelName={loggedInChannelName}\n              loggedInChannelShortId={loggedInChannelShortId}\n            />\n          </div>\n          <div className={'fill'}>\n            <input\n              type='text'\n              className='input-text input--full-width'\n              name='claim'\n              placeholder='your-url-here'\n              onChange={this.handleInput}\n              value={claim}\n            />\n          </div>\n        </div>\n        <FormFeedbackDisplay\n          errorMessage={urlError}\n          defaultMessage={'Choose a custom url'}\n        />\n      </div>\n    );\n  }\n}\n\nexport default PublishUrlInput;\n"
  },
  {
    "path": "client/src/containers/SEO/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nexport default connect(null, null)(View);\n"
  },
  {
    "path": "client/src/containers/SEO/view.jsx",
    "content": "import React from 'react';\nimport Helmet from 'react-helmet';\nimport PropTypes from 'prop-types';\n\nimport createPageTitle from '../../utils/createPageTitle';\nimport createMetaTags from '../../utils/createMetaTags';\nimport oEmbed from '../../utils/oEmbed.js';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\n\nimport siteConfig from '@config/siteConfig.json';\nconst { details: { host } } = siteConfig;\n\nclass SEO extends React.Component {\n  render () {\n    // props from parent\n    const { asset, channel, pageUri } = this.props;\n    let { pageTitle } = this.props;\n    // create page title, tags, and canonical link\n    pageTitle = createPageTitle(pageTitle);\n    const metaTags = createMetaTags({\n      asset,\n      channel,\n    });\n    const canonicalLink = `${host}${createCanonicalLink({\n      asset: asset ? { ...asset.claimData, shortId: asset.shortId } : undefined,\n      channel,\n      page : pageUri,\n    })}`;\n    // render results\n    return (\n      <Helmet\n        title={pageTitle}\n        meta={metaTags}\n        link={[\n          {\n            rel : 'canonical',\n            href: canonicalLink,\n          },\n          oEmbed.json(host, canonicalLink),\n        ]}\n      />\n    );\n  }\n}\n\nSEO.propTypes = {\n  pageTitle: PropTypes.string,\n  pageUri  : PropTypes.string,\n  channel  : PropTypes.object,\n  asset    : PropTypes.object,\n};\n\nexport default SEO;\n"
  },
  {
    "path": "client/src/containers/SiteDescription/index.jsx",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ site }) => {\n  return {\n    siteDescription: site.description,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/containers/SiteDescription/view.jsx",
    "content": "import React from 'react';\n\nclass SiteDescription extends React.Component {\n  render () {\n    return (\n      <div className={'site-description'}>\n        <p className={'text--extra-small'}>{this.props.siteDescription}</p>\n      </div>\n    );\n  }\n}\n\nexport default SiteDescription;\n"
  },
  {
    "path": "client/src/index.js",
    "content": "import React from 'react';\nimport { hydrate } from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { createStore, applyMiddleware, compose } from 'redux';\nimport { BrowserRouter } from 'react-router-dom';\nimport createSagaMiddleware from 'redux-saga';\nimport Reducers from '@reducers';\nimport Sagas from '@sagas';\nimport App from '@app';\nimport GAListener from '@components/GAListener';\n\n// import scss so webpack will build it\nimport '../scss/all.scss';\n\n// get the state from a global variable injected into the server-generated HTML\nconst preloadedState = window.__PRELOADED_STATE__ || null;\n\n// Allow the passed state to be garbage-collected\ndelete window.__PRELOADED_STATE__;\n\n// create and apply middleware\nconst sagaMiddleware = createSagaMiddleware();\nconst middleware = applyMiddleware(sagaMiddleware);\nconst reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;\n\n// create the store\nlet store;\nif (preloadedState) {\n  store = createStore(Reducers, preloadedState, reduxMiddleware);\n} else {\n  store = createStore(Reducers, reduxMiddleware);\n}\n\nsagaMiddleware.run(Sagas.rootSaga);\n\n// render the app\nhydrate(\n  <Provider store={store}>\n    <BrowserRouter>\n      <GAListener>\n        <App />\n      </GAListener>\n    </BrowserRouter>\n  </Provider>,\n  document.getElementById('react-app')\n);\n"
  },
  {
    "path": "client/src/pages/AboutPage/index.jsx",
    "content": "import React from 'react';\nimport { withRouter } from 'react-router';\nimport PageLayout from '@components/PageLayout';\nimport HorizontalSplit from '@components/HorizontalSplit';\nimport AboutSpeechOverview from '@components/AboutSpeechOverview';\nimport AboutSpeechDetails from '@components/AboutSpeechDetails';\n\nclass AboutPage extends React.Component {\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'About'}\n        pageUri={'about'}\n      >\n        <HorizontalSplit\n          collapseOnMobile\n          leftSide={<AboutSpeechOverview />}\n          rightSide={<AboutSpeechDetails />}\n        />\n      </PageLayout>\n    );\n  }\n}\n\nexport default withRouter(AboutPage);\n"
  },
  {
    "path": "client/src/pages/ContentPageWrapper/index.jsx",
    "content": "import { connect } from 'react-redux';\nimport { onHandleShowPageUri } from '../../actions/show';\nimport View from './view';\n\nconst mapStateToProps = ({ show }) => {\n  return {\n    error      : show.request.error,\n    requestType: show.request.type,\n  };\n};\n\nconst mapDispatchToProps = {\n  onHandleShowPageUri,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/pages/ContentPageWrapper/view.jsx",
    "content": "import React from 'react';\nimport ErrorPage from '@pages/ErrorPage';\nimport ShowAssetLite from '@pages/ShowAssetLite';\nimport ShowAssetDetails from '@pages/ShowAssetDetails';\nimport ShowChannel from '@pages/ShowChannel';\nimport { withRouter, Redirect } from 'react-router-dom';\n\nimport {\n  CHANNEL,\n  ASSET_LITE,\n  ASSET_DETAILS,\n  SPECIAL_ASSET,\n} from '../../constants/show_request_types';\n\nclass ContentPageWrapper extends React.Component {\n  componentDidMount () {\n    const { onHandleShowPageUri, match, homeChannel } = this.props;\n    //onHandleShowPageUri(homeChannel ? { claim: homeChannel } : match.params);\n  }\n  componentWillReceiveProps (nextProps) {\n    if (nextProps.match.params !== this.props.match.params) {\n      //this.props.onHandleShowPageUri(nextProps.match.params);\n    }\n  }\n  render () {\n    const { error, requestType, match } = this.props;\n    const { params } = match;\n    const { claim, identifier } = params;\n    if (identifier && claim) {\n      return <Redirect to={`https://lbry.tv/${identifier}/${claim}`} />;\n    } else if (identifier) {\n      // return <Redirect to={`https://lbry.tv/${identifier}/`} />\n    } else {\n      return <Redirect to={`https://lbry.tv/${claim}`} />;\n    }\n    if (error) {\n      return (\n        <ErrorPage error={error} />\n      );\n    }\n    switch (requestType) {\n      case CHANNEL:\n        // return <ShowChannel />;\n      case ASSET_LITE:\n        // return <ShowAssetLite />;\n      case ASSET_DETAILS:\n        // return <ShowAssetDetails />;\n      case SPECIAL_ASSET:\n        // return <ShowChannel />;\n      default:\n        return <p>loading...</p>;\n    }\n  }\n};\n\nexport default withRouter(ContentPageWrapper);\n"
  },
  {
    "path": "client/src/pages/EditPage/index.js",
    "content": "import { connect } from 'react-redux';\nimport { setUpdateTrue, setHasChanged, updateMetadata, clearFile } from '../../actions/publish';\nimport { onHandleShowPageUri } from '../../actions/show';\nimport { selectAsset } from '../../selectors/show';\nimport View from './view';\n\nconst mapStateToProps = (props) => {\n  const { show } = props;\n  return {\n    asset    : selectAsset(show),\n    myChannel: props.channel.loggedInChannel.name,\n    isUpdate : props.publish.isUpdate,\n  };\n};\n\nconst mapDispatchToProps = {\n  updateMetadata,\n  onHandleShowPageUri,\n  setUpdateTrue,\n  setHasChanged,\n  clearFile,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/pages/EditPage/view.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport { Redirect } from 'react-router-dom';\nimport PublishTool from '@containers/PublishTool';\n\nclass EditPage extends React.Component {\n  componentDidMount () {\n    const {asset, match, onHandleShowPageUri, setUpdateTrue, setHasChanged, updateMetadata} = this.props;\n    onHandleShowPageUri(match.params);\n    setUpdateTrue();\n    if (asset) {\n      ['title', 'description', 'license', 'licenseUrl', 'nsfw'].forEach(meta => updateMetadata(meta, asset.claimData[meta]));\n    }\n    setHasChanged(false);\n  }\n  componentWillUnmount () {\n    this.props.clearFile();\n  }\n  render () {\n    const { myChannel, asset } = this.props;\n    // redirect if user does not own this claim\n    if (\n      !myChannel || (\n        asset &&\n        asset.claimsData &&\n        asset.claimsData.channelName &&\n        asset.claimsData.channelName !== myChannel\n      )\n    ) {\n      return (<Redirect to={'/'} />);\n    }\n    return (\n      <PageLayout\n        pageTitle={'Edit claim'}\n        pageUri={'edit'}\n      >\n        <PublishTool />\n      </PageLayout>\n    );\n  }\n};\n\nexport default EditPage;\n"
  },
  {
    "path": "client/src/pages/ErrorPage/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport PageLayout from '@components/PageLayout';\n\nclass ErrorPage extends React.Component {\n  render () {\n    const { error } = this.props;\n    return (\n      <PageLayout\n        pageTitle={'Error'}\n        pageUri={'error'}\n      >\n        <p>{error}</p>\n      </PageLayout>\n    );\n  }\n};\n\nErrorPage.propTypes = {\n  error: PropTypes.string.isRequired,\n};\n\nexport default ErrorPage;\n"
  },
  {
    "path": "client/src/pages/FaqPage/index.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport Row from '@components/Row';\n\nclass FaqPage extends React.Component {\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'Frequently Asked Questions'}\n        pageUri={'tos'}\n      >\n        <Row>\n          <Row>\n            <h1>Frequently Asked Questions</h1>\n          </Row>\n          <Row>\n            <h3>What is spee.ch?</h3>\n            <p>Spee.ch is a media-hosting site that reads from and publishes content to the <a href='http://lbry.com/'>LBRY blockchain</a>.</p>\n          </Row>\n          <Row>\n            <h3>OK But Why Should I Care?</h3>\n            <p>Spee.ch is a fast and easy way to host your images, videos, and other content. What makes this different from other similar sites is that Spee.ch is hosted on the LBRY blockchain. That means it is impossible for your content to be censored via digital means. Even if we took down Spee.ch today, all content would remain immutably stored on the LBRY blockchain.</p>\n            <p>Blockchain technology doesn’t solve <a href='https://xkcd.com/538/'>the 5 dollar wrench attack</a>, but it solves just about every other problem in media hosting and distribution.</p>\n            <p>Even better - you can host your own clone of Spee.ch to get even more control over your content. <a href='https://github.com/lbryio/spee.ch/blob/master/README.md'>CLICK HERE FOR INFO</a>.</p>\n            <p>Spee.ch is just the beginning of what will soon be a vibrant ecosystem of LBRY-powered apps. Use LBRY and you’re one step closer to true freedom.</p>\n          </Row>\n          <Row>\n            <h3>How to Use spee.ch</h3>\n            <p>It’s easy. Drag the image or video file of your choice into the center of the spee.ch homepage.</p>\n            <p>Spee.ch is currently best suited for web optimized MP4 video and standard image filetypes (JPEG, PNG, GIF).</p>\n            <p>If you want to refer to a piece of content repeatedly, or to build a collection of related content, you could create a channel. Channels work both for private collections and for public repositories. There’s more info about how to do this <a href='https://spee.ch/login'>on the channel page</a>.</p>\n            <p>Published files will be viewable and embeddable with any web browser and accesible in the LBRY app. You can also use spee.ch to view free and non-NSFW content published on LBRY network from LBRY app. You just need to replace \"lbry://\" with \"http://spee.ch/\" in the URL.</p>\n          </Row>\n          <Row>\n            <h3>How Long Does Content Stay on Spee.ch?</h3>\n            <p>All content uploaded on spee.ch is guaranteed to stay up for at least 10 years with no maintenance. Future updates will likely extend that time horizon further as blockchain technology improves.</p>\n          </Row>\n          <Row>\n            <h3>Contribute</h3>\n            <p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>\n            <p>If you want to improve spee.ch, join <a href='https://chat.lbry.com/'>our discord channel</a> or solve one of our <a href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>\n          </Row>\n        </Row>\n      </PageLayout>\n    );\n  }\n}\n\nexport default FaqPage;\n"
  },
  {
    "path": "client/src/pages/FourOhFourPage/index.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\n\nclass FourOhForPage extends React.Component {\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'404'}\n        pageUri={'/404'}\n      >\n        <h2>404</h2>\n        <p>That page does not exist</p>\n      </PageLayout>\n    );\n  }\n};\n\nexport default FourOhForPage;\n"
  },
  {
    "path": "client/src/pages/HomePage/index.js",
    "content": "import { connect } from 'react-redux';\nimport { onHandleShowHomepage } from '../../actions/show';\nimport { clearFile } from '../../actions/publish';\nimport View from './view';\n\nconst mapStateToProps = ({ show, site, channel, publish }) => {\n  return {\n    error      : show.request.error,\n    requestType: show.request.type,\n    homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,\n    isUpdate   : publish.isUpdate,\n  };\n};\n\nconst mapDispatchToProps = {\n  onHandleShowHomepage,\n  clearFile,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/pages/HomePage/view.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport PublishTool from '@containers/PublishTool';\nimport ContentPageWrapper from '@pages/ContentPageWrapper';\n\nclass HomePage extends React.Component {\n  componentWillUnmount () {\n    this.props.clearFile();\n  }\n  render () {\n    const { homeChannel } = this.props;\n    return homeChannel ? (\n      <ContentPageWrapper homeChannel={homeChannel} />\n    ) : (\n      <PageLayout\n        pageTitle={'Speech'}\n        pageUri={''}\n      >\n        <PublishTool />\n      </PageLayout>\n    );\n  }\n};\n\nexport default HomePage;\n"
  },
  {
    "path": "client/src/pages/LoginPage/index.jsx",
    "content": "import {connect} from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ channel }) => {\n  return {\n    loggedInChannelName: channel.loggedInChannel.name,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/pages/LoginPage/view.jsx",
    "content": "import React from 'react';\nimport { withRouter } from 'react-router-dom';\nimport PageLayout from '@components/PageLayout';\nimport HorizontalSplit from '@components/HorizontalSplit';\n\nimport ChannelAbout from '@components/ChannelAbout';\nimport ChannelTools from '@containers/ChannelTools';\n\nclass LoginPage extends React.Component {\n  componentWillReceiveProps (newProps) {\n    // re-route the user to the homepage if the user is logged in\n    if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {\n      this.props.history.push(`/`);\n    }\n  }\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'Login'}\n        pageUri={'login'}\n      >\n        <HorizontalSplit\n          collapseOnMobile\n          leftSide={<ChannelAbout />}\n          rightSide={<ChannelTools />}\n        />\n      </PageLayout>\n    );\n  }\n};\n\nexport default withRouter(LoginPage);\n"
  },
  {
    "path": "client/src/pages/MultisitePage/index.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\n\nconst MultisiteContent = () => {\n  return (\n    <div>\n      <p className='text--pull-quote'>Introducing Spee.ch Multisite</p>\n      <p>Hi there!  My name is <a href={'https://github.com/billbitt'} target={'_blank'}>Bill</a>, and I’d like to speak with you about Spee.ch.  No, not ‘speech,’ ‘<i><a href={'https://spee.ch'} target={'_blank'}>Spee.ch.</a></i>’ You know what, just read on...</p>\n      <h2>A Little Background</h2>\n      <p>Wow, time flies!  A little over a year ago Spee.ch was nothing more than a glimmer in the eye of LBRY CEO Jeremy Kaufman.  At that time, the <a href={'https://lbry.com/faq/what-is-lbry'} target={'_blank'}>LBRY protocol</a> was still so early in its development, that there were no web-based applications for interacting with the LBRY blockchain. But then, something beautiful happened.  On March 29th, 2017, Jeremy sat down with Jack, and together they <a href={'https://www.youtube.com/watch?v=C9LCapt_OYw'} target={'_blank'}>live coded a single-page PHP site</a> that could publish images to the LBRY network.  And just like that, Spee.ch was born!</p>\n      <p>Being that LBRY is an open source project, Jeremy ended the session by inviting community members who were interested in the project to take the reigns and see where Spee.ch could go.  I was one of the devs that did just that, and it wasn’t long before I was on a weekly call dedicated to this project with contributors from around the world.</p>\n      <p>At this point in time, the vision for Spee.ch was pretty simple: create a web-based hosting service that used the LBRY network as a database for free image and video sharing.  In other words, an ‘imgur on the blockchain.’</p>\n      <h2>Growth</h2>\n      <p>You might be wondering, “So, what has the Spee.ch team been doing since then?”. Well, that is a great question. I’m glad you asked.</p>\n      <p>As it turned out, the initial single-serving site was only the beginning.  We wanted to add more features, improve user experience, and continue to rapidly innovate on new ideas to explore what web-based image-hosting on the blockchain could look like.  And now -- a couple of re-designs, <a href={'https://github.com/lbryio/spee.ch'} target={'_blank'}>1,428 commits</a>, and <a href={'https://github.com/lbryio/spee.ch/graphs/contributors'} target={'_blank'}>18 contributors</a> later (as of the time of this writing) -- we’ve been through a lot of changes.  We changed the URL scheme, switched out the PHP for Javascript (sorry Jeremy!), added more HTML pages, removed those HTML pages, added Handlebars, removed most of Handlebars, added React, and... you get the picture.</p>\n      <p>It’s been a lot of work, and through all of these changes, we have been guided by our original vision: develop a free web app that allows users to share images and video using a blockchain.</p>\n      <p>However, we ask ourselves constantly: what else can we be doing?  What can we be doing differently?  What features can we be doing better?  And it is those kinds of questions that lead us to this post.</p>\n      <h2>A New Initiative</h2>\n      <p>As Spee.ch developed, we were lucky to find an amazing community spring up around the project that contributed bug reports, bug fixes, feature requests, pull requests, etc., but ultimately we are limited by the hours we have in the day, and while some requests get prioritized, others get shelved. </p>\n      <p>So we started wondering:  What if instead of having the community help us build our platform, we started helping them build theirs?  We started mulling this over, and the more we thought about it the more we liked it. And thus, Spee.ch Multisite was born.</p>\n      <h2>Spee.ch Multisite</h2>\n      <p>The vision for Spee.ch Multisite is to maintain a foundational codebase that will support a greater variety of content-sharing web apps built on LBRY, allowing these apps to publish and retrieve content from the network via the blockchain.</p>\n      <h3>Run Your Own Spee.ch!</h3>\n      <p>Ok, here’s the tl:dr: the purpose of the Spee.ch Multisite initiative is to enable you to run your own version of Spee.ch.</p>\n      <p>Spee.ch Multisite will provide a helpful set of basic code to get you going, but we purposefully want to give you control and provide a sandbox in which you can develop the look, content, and features for your site.  The shared code base will be developed to support you in that quest. </p>\n      <p>So if you don’t want your site called or looking anything like Spee.ch, we encourage that! Don’t hesitate to make it your own!</p>\n      <h3>For the Community by the Community</h3>\n      <p>Initially, sites built on Spee.ch Multisite will look a lot like Spee.ch, but you will be able to add custom pages, update the look of components, and limit the content on your spee.ch site as you see fit.</p>\n      <p>Over time, it is our hope that the project will grow to incorporate many more components and features developed by us and the community to support a wide variety of functionalities beyond what the current spee.ch site is capable of.</p>\n      <h3>A Common Codebase</h3>\n      <p>If you have been following the project, you may have already noticed that the original github repository has grown into two: <a href={'https://github.com/lbryio/www.spee.ch'} target={'_blank'}>www.spee.ch</a> and <a href={'https://github.com/lbryio/spee.ch'} target={'_blank'}>spee.ch</a>.  I will save the specifics for a future tech-focused blog post in the coming weeks, but the reason for these changes is to modularise the code so that is it easier for anyone who wants to run their own version of Spee.ch to do so, and to be able to customize their Spee.ch to their liking.</p>\n      <h3>What About the Flagship Spee.ch Site?</h3>\n      <p>Don’t worry!  If you like using <a href={'https://spee.ch'} target={'_blank'}>Spee.ch</a> and have no intention of running your own site, we will still be here running it for you!  We are dedicated to pushing it forward and using it as patient zero for all additions to the Spee.ch Multisite codebase.</p>\n      <h2>Join Us</h2>\n      <p>Friday, May 18, we will be hosting a live demo showcasing the alpha version of Spee.ch Multisite.  It’s still quite young, but that’s the point: we want to realize this vision together.</p>\n      <p><b><a href={'https://speech.rsvpify.com/'} target={'_blank'}>CLICK HERE TO RSVP!</a></b></p>\n      <p>At this first demonstration, we will walk through preparing a server environment, installing LBRY and Spee.ch, and how to make local changes to your Spee.ch instance.  Details below:</p>\n      <ul>\n        <li>When: Friday, May 18, 2018</li>\n        <li>Time: 5:00 p.m. PST</li>\n        <li>Where: Google Hangouts</li>\n        <li>Link: <a href={'https://meet.google.com/aex-ghqg-kcs'} target={'_blank'}>meet.google.com/aex-ghqg-kcs</a></li>\n        <li>System Requirements: If you have a server, please make sure you have MySql, Node and NPM installed. If you need help installing the above, or if you need a server to run your own instance on, please join the Hangout 30 minutes ahead of time and we will help get you set up =]</li>\n        <li>Questions: hello@lbry.com</li>\n      </ul>\n    </div>\n  );\n};\n\nclass MultisitePage extends React.Component {\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'Multisite'}\n        pageUri={'/multisite'}\n      >\n        <MultisiteContent />\n      </PageLayout>\n    );\n  }\n}\n\nexport default MultisitePage;\n"
  },
  {
    "path": "client/src/pages/PopularPage/index.jsx",
    "content": "import { connect } from 'react-redux';\nimport { onHandleShowHomepage } from '../../actions/show';\nimport View from './view';\n\nconst mapStateToProps = ({ show, site, channel }) => {\n  return {\n    error      : show.request.error,\n    requestType: show.request.type,\n    homeChannel: 'special:trending',\n  };\n};\n\nconst mapDispatchToProps = {\n  onHandleShowHomepage,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(View);\n"
  },
  {
    "path": "client/src/pages/PopularPage/view.jsx",
    "content": "import React from 'react';\nimport ContentPageWrapper from '@pages/ContentPageWrapper';\n\nclass PopularPage extends React.Component {\n  componentDidMount () {\n    this.props.onHandleShowHomepage(this.props.match.params);\n  }\n\n  componentWillReceiveProps (nextProps) {\n    if (nextProps.match.params !== this.props.match.params) {\n      this.props.onHandleShowHomepage(nextProps.match.params);\n    }\n  }\n\n  render () {\n    const { homeChannel } = this.props;\n    return (\n      <ContentPageWrapper homeChannel={homeChannel} />\n    );\n  }\n};\n\nexport default PopularPage;\n"
  },
  {
    "path": "client/src/pages/ShowAssetDetails/index.js",
    "content": "import { connect } from 'react-redux';\nimport { selectAsset, selectDetailsExpanded } from '../../selectors/show';\nimport { toggleDetailsExpanded } from '../../actions/show';\n\nimport View from './view';\n\nconst mapStateToProps = state => {\n  return {\n    asset: selectAsset(state.show),\n    detailsExpanded: selectDetailsExpanded(state),\n  };\n};\n\nconst mapDispatchToProps = dispatch => {\n  return {\n    onToggleDetailsExpanded: value => {\n      dispatch(toggleDetailsExpanded(value));\n    },\n  };\n};\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(View);\n"
  },
  {
    "path": "client/src/pages/ShowAssetDetails/view.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport * as Icon from 'react-feather';\nimport AssetDisplay from '@containers/AssetDisplay';\nimport AssetBlocked from '@containers/AssetBlocked';\nimport AssetInfo from '@containers/AssetInfo';\nimport ErrorPage from '@pages/ErrorPage';\nimport AssetTitle from '@containers/AssetTitle';\n\nclass ShowAssetDetails extends React.Component {\n\n  constructor (props) {\n    super(props);\n    this.toggleExpandDetails = this.toggleExpandDetails.bind(this);\n  }\n\n  toggleExpandDetails () {\n    this.props.onToggleDetailsExpanded(!this.props.detailsExpanded);\n  }\n\n  render () {\n    const { asset, detailsExpanded } = this.props;\n    if (asset) {\n      const { claimData: { name, blocked } } = asset;\n      if (!blocked) {\n        return (\n          <PageLayout\n            pageTitle={`${name} - details`}\n            asset={asset}\n          >\n            <div className='asset-main'>\n              <AssetTitle />\n              <AssetDisplay />\n              <div>\n                <button className='collapse-button' onClick={this.toggleExpandDetails}>\n                  {detailsExpanded ? <Icon.MinusCircle /> : <Icon.PlusCircle className='plus-icon' /> }\n                </button>\n              </div>\n            </div>\n            {detailsExpanded && <AssetInfo />}\n\n          </PageLayout>\n        );\n      } else {\n        return (\n          <PageLayout>\n            <div className=\"asset-main\">\n              <AssetBlocked />\n            </div>\n          </PageLayout>\n        );\n      }\n    }\n    return (\n      <ErrorPage error={'loading asset data...'} />\n    );\n  }\n};\n\nexport default ShowAssetDetails;\n"
  },
  {
    "path": "client/src/pages/ShowAssetLite/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ show }) => {\n  // select request info\n  const requestId = show.request.id;\n  // select asset info\n  let asset;\n  const request = show.requestList[requestId] || null;\n  const assetList = show.assetList;\n  if (request && assetList) {\n    const assetKey = request.key;  // note: just store this in the request\n    asset = assetList[assetKey] || null;\n  };\n  // return props\n  return {\n    asset,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/pages/ShowAssetLite/view.jsx",
    "content": "import React from 'react';\nimport { Link } from 'react-router-dom';\nimport PageLayoutShowLite from '@components/PageLayoutShowLite';\nimport AssetDisplay from '@containers/AssetDisplay';\nimport SpaceAround from '@components/SpaceAround';\nimport VerticalSplit from '@components/VerticalSplit';\n\nconst AssetLiteFooter = ({ name, claimId }) => {\n  return (\n    <SpaceAround>\n      <p className={'text--extra-small'}>\n        <Link className='link--primary' to={`/${claimId}/${name}`}> hosted on spee.ch</Link> via the <a  className='link--primary' href={'https://lbry.com/get'} target={'_blank'}>LBRY</a> blockchain\n      </p>\n    </SpaceAround>\n  );\n};\n\nclass ShowLite extends React.Component {\n  render () {\n    const { asset } = this.props;\n    if (asset) {\n      const { name, claimId } = asset.claimData;\n      return (\n        <PageLayoutShowLite\n          pageTitle={name}\n          asset={asset}\n        >\n          <VerticalSplit\n            top={<AssetDisplay />}\n            bottom={\n              <AssetLiteFooter\n                name={name}\n                claimId={claimId}\n              />\n            }\n          />\n        </PageLayoutShowLite>\n      );\n    }\n    return (\n      <div>\n        <p className={'text--secondary'}>loading asset data...</p>\n      </div>\n    );\n  }\n}\n\nexport default ShowLite;\n"
  },
  {
    "path": "client/src/pages/ShowChannel/index.js",
    "content": "import { connect } from 'react-redux';\nimport View from './view';\n\nconst mapStateToProps = ({ show, site, channel }) => {\n  // select request info\n  const requestId = show.request.id;\n  // select request\n  const previousRequest = show.requestList[requestId] || null;\n\n  // select channel\n  let thisChannel;\n  if (previousRequest) {\n    const channelKey = previousRequest.key;\n    thisChannel = show.channelList[channelKey] || null;\n  }\n  return {\n    channel    : thisChannel,\n    homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,\n  };\n};\n\nexport default connect(mapStateToProps, null)(View);\n"
  },
  {
    "path": "client/src/pages/ShowChannel/view.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport ErrorPage from '@pages/ErrorPage';\nimport ChannelInfoDisplay from '@components/ChannelInfoDisplay';\nimport ChannelClaimsDisplay from '@containers/ChannelClaimsDisplay';\nimport Row from '@components/Row';\n\nclass ShowChannel extends React.Component {\n  render () {\n    const { channel, homeChannel } = this.props;\n    if (channel) {\n      const { name, longId, shortId } = channel;\n      return (\n        <PageLayout\n          pageTitle={name}\n          channel={channel}\n        >\n          {!homeChannel && (\n            <Row>\n              <ChannelInfoDisplay\n                name={name}\n                longId={longId}\n                shortId={shortId}\n              />\n            </Row>\n          )}\n          <Row>\n            <ChannelClaimsDisplay />\n          </Row>\n        </PageLayout>\n      );\n    }\n    return (\n      <ErrorPage error={'loading channel data...'} />\n    );\n  }\n};\n\nexport default ShowChannel;\n"
  },
  {
    "path": "client/src/pages/TosPage/index.jsx",
    "content": "import React from 'react';\nimport PageLayout from '@components/PageLayout';\nimport Row from '@components/Row';\n\nclass TosPage extends React.Component {\n  render () {\n    return (\n      <PageLayout\n        pageTitle={'Terms of Service'}\n        pageUri={'tos'}\n      >\n        <Row>\n          <Row>\n            <h1>Terms of Service</h1>\n            <p>Last updated: September 25, 2018</p>\n            <p>Please read these Terms of Service (\"Terms\", \"Terms of Service\") carefully before using the <a className='link--primary' href='https://spee.ch'>https://spee.ch</a> website (the \"Service\") operated by <a className='link--primary' href='https://lbry.com'>LBRY INC</a> (\"us\", \"we\", or \"our\").</p>\n            <p>Your access to and use of the Service is conditioned upon your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who wish to access or use the Service.</p>\n            <p>By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you do not have permission to access the Service.</p>\n          </Row>\n          <Row>\n            <h3>Links To Other Web Sites</h3>\n            <p>Our Service may contain links to third party web sites or services that are not owned or controlled by <a className='link--primary' href='https://lbry.com'>LBRY INC</a></p>\n            <p><a className='link--primary' href='https://lbry.com'>LBRY INC</a> has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites.</p>\n            <p>You acknowledge and agree that <a className='link--primary' href='https://lbry.com'>LBRY INC</a> shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such third party web sites or services.</p>\n            <p>We strongly advise you to read the terms and conditions and privacy policies of any third party web sites or services that you visit.</p>\n          </Row>\n          <Row>\n            <h3>Termination</h3>\n            <p>We may terminate or suspend your access to the Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.</p>\n            <p>All provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.</p>\n          </Row>\n          <Row>\n            <h3>Indemnification</h3>\n            <p>You agree to defend, indemnify and hold harmless LBRY INC and its licensee and licensors, and their employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, and expenses (including but not limited to attorney's fees), resulting from or arising out of a) your use and access of the Service, or b) a breach of these Terms.</p>\n          </Row>\n          <Row>\n            <h3>Limitation Of Liability</h3>\n            <p>In no event shall <a className='link--primary' href='https://lbry.com'>LBRY INC</a>, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.</p>\n          </Row>\n          <Row>\n            <h3>Disclaimer</h3>\n            <p>Your use of the Service is at your sole risk. The Service is provided on an \"AS IS\" and \"AS AVAILABLE\" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.</p>\n            <p><a className='link--primary' href='https://lbry.com'>LBRY INC</a> its subsidiaries, affiliates, and its licensors do not warrant that a) the Service will function uninterrupted, secure or available at any particular time or location; b) any errors or defects will be corrected; c) the Service is free of viruses or other harmful components; or d) the results of using the Service will meet your requirements.</p>\n          </Row>\n          <Row>\n            <h3>Exclusions</h3>\n            <p>Some jurisdictions do not allow the exclusion of certain warranties or the exclusion or limitation of liability for consequential or incidental damages, so the limitations above may not apply to you.</p>\n          </Row>\n          <Row>\n            <h3>Governing Law</h3>\n            <p>These Terms shall be governed and construed in accordance with the laws of Delaware, United States, without regard to its conflict of law provisions.</p>\n            <p>Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have had between us regarding the Service.</p>\n          </Row>\n          <Row>\n            <h3>Changes</h3>\n            <p>We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.</p>\n            <p>By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use the Service.</p>\n          </Row>\n          <Row>\n            <h3>Contact Us</h3>\n            <p>If you have any questions about these Terms, please <a className='link--primary' href='mailto:hello@lbry.com'>contact us</a>.</p>\n          </Row>\n        </Row>\n      </PageLayout>\n    );\n  }\n}\n\nexport default TosPage;\n"
  },
  {
    "path": "client/src/reducers/channel.js",
    "content": "import * as actions from '../constants/channel_action_types';\n\nconst initialState = {\n  loggedInChannel: {\n    name   : null,\n    shortId: null,\n    longId : null,\n  },\n};\n\nexport default function (state = initialState, action) {\n  switch (action.type) {\n    case actions.CHANNEL_UPDATE:\n      return Object.assign({}, state, {\n        loggedInChannel: action.data,\n      });\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "client/src/reducers/channelCreate.js",
    "content": "import * as actions from '../constants/channel_create_action_types';\n\nconst initialState = {\n  name: {\n    value: '',\n    error: '',\n  },\n  password: {\n    value: '',\n    error: '',\n  },\n  status: null,\n};\n\nexport default function (state = initialState, action) {\n  switch (action.type) {\n    case actions.CHANNEL_CREATE_UPDATE_NAME:\n      return Object.assign({}, state, {\n        name: Object.assign({}, state.name, {\n          [action.data.name]: action.data.value,\n        }),\n      });\n    case actions.CHANNEL_CREATE_UPDATE_PASSWORD:\n      return Object.assign({}, state, {\n        password: Object.assign({}, state.password, {\n          [action.data.name]: action.data.value,\n        }),\n      });\n    case actions.CHANNEL_CREATE_UPDATE_STATUS:\n      return Object.assign({}, state, {\n        status: action.data,\n      });\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "client/src/reducers/index.js",
    "content": "// modules\nimport { combineReducers } from 'redux';\n\n// local modules\nimport PublishReducer from './publish';\nimport ChannelReducer from './channel';\nimport ShowReducer from './show';\nimport SiteReducer from './site';\nimport ChannelCreateReducer from './channelCreate';\n\nexport default combineReducers({\n  channel      : ChannelReducer,\n  channelCreate: ChannelCreateReducer,\n  publish      : PublishReducer,\n  show         : ShowReducer,\n  site         : SiteReducer,\n});\n"
  },
  {
    "path": "client/src/reducers/publish.js",
    "content": "import * as actions from '../constants/publish_action_types';\nimport { LOGIN } from '../constants/publish_channel_select_states';\n\nimport siteConfig from '@config/siteConfig.json';\n\n// parse inputs\nlet disabledConfig = false;\nlet disabledMessageConfig = 'none';\nlet thumbnailChannel = '';\nlet thumbnailChannelId = '';\nif (siteConfig) {\n  if (siteConfig.publishing) {\n    disabledConfig = siteConfig.publishing.disabled;\n    disabledMessageConfig = siteConfig.publishing.disabledMessage;\n    thumbnailChannel = siteConfig.publishing.thumbnailChannel;\n    thumbnailChannelId = siteConfig.publishing.thumbnailChannelId;\n  }\n}\n\n// create initial state\nconst initialState = {\n  disabled: disabledConfig,\n  disabledMessage: disabledMessageConfig,\n  publishInChannel: false,\n  selectedChannel: LOGIN,\n  showMetadataInputs: false,\n  status: {\n    status: null,\n    message: null,\n  },\n  error: {\n    file: null,\n    url: null,\n    channel: null,\n  },\n  file: null,\n  claim: '',\n  metadata: {\n    title: '',\n    description: '',\n    license: '',\n    licenseUrl: '',\n    tags: [],\n  },\n  isUpdate: false,\n  hasChanged: false,\n  thumbnail: null,\n  thumbnailChannel,\n  thumbnailChannelId,\n};\n\nexport default function(state = initialState, action) {\n  switch (action.type) {\n    case actions.FILE_SELECTED:\n      return Object.assign({}, state.isUpdate ? state : initialState, {\n        // note: clears to initial state\n        file: action.data,\n        hasChanged: true,\n      });\n    case actions.FILE_CLEAR:\n      return initialState;\n    case actions.METADATA_UPDATE:\n      return Object.assign({}, state, {\n        metadata: Object.assign({}, state.metadata, {\n          [action.data.name]: action.data.value,\n        }),\n        hasChanged: true,\n      });\n    case actions.CLAIM_UPDATE:\n      return Object.assign({}, state, {\n        claim: action.data,\n        hasChanged: true,\n      });\n    case actions.SET_PUBLISH_IN_CHANNEL:\n      return Object.assign({}, state, {\n        publishInChannel: action.channel,\n        hasChanged: true,\n      });\n    case actions.PUBLISH_STATUS_UPDATE:\n      return Object.assign({}, state, {\n        status: action.data,\n      });\n    case actions.ERROR_UPDATE:\n      return Object.assign({}, state, {\n        error: Object.assign({}, state.error, {\n          [action.data.name]: action.data.value,\n        }),\n      });\n    case actions.SELECTED_CHANNEL_UPDATE:\n      return Object.assign({}, state, {\n        selectedChannel: action.data,\n      });\n    case actions.TOGGLE_METADATA_INPUTS:\n      return {\n        ...state,\n        showMetadataInputs: action.data,\n      };\n    case actions.THUMBNAIL_NEW:\n      return {\n        ...state,\n        thumbnail: action.data,\n        hasChanged: true,\n      };\n    case actions.SET_UPDATE_TRUE:\n      return {\n        ...state,\n        isUpdate: true,\n      };\n    case actions.SET_HAS_CHANGED:\n      return {\n        ...state,\n        hasChanged: action.data,\n      };\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "client/src/reducers/show.js",
    "content": "import * as actions from '../constants/show_action_types';\nimport { LOCAL_CHECK, ERROR } from '../constants/asset_display_states';\n\nconst initialState = {\n  request: {\n    error: null,\n    type: null,\n    id: null,\n  },\n  requestList: {},\n  channelList: {},\n  assetList: {},\n  displayAsset: {\n    error: null,\n    status: LOCAL_CHECK,\n  },\n  detailsExpanded: true,\n};\n\nexport default function(state = initialState, action) {\n  switch (action.type) {\n    // handle request\n    case actions.REQUEST_ERROR:\n      return Object.assign({}, state, {\n        request: Object.assign({}, state.request, {\n          error: action.data,\n        }),\n      });\n    case actions.REQUEST_UPDATE:\n      return Object.assign({}, state, {\n        request: Object.assign({}, state.request, {\n          type: action.data.requestType,\n          id: action.data.requestId,\n        }),\n      });\n    // store requests\n    case actions.REQUEST_LIST_ADD:\n      return Object.assign({}, state, {\n        requestList: Object.assign({}, state.requestList, {\n          [action.data.id]: {\n            error: action.data.error,\n            key: action.data.key,\n          },\n        }),\n      });\n    // asset data\n    case actions.ASSET_ADD:\n      return Object.assign({}, state, {\n        assetList: Object.assign({}, state.assetList, {\n          [action.data.id]: {\n            error: action.data.error,\n            name: action.data.name,\n            claimId: action.data.claimId,\n            shortId: action.data.shortId,\n            claimData: action.data.claimData,\n            claimViews: action.data.claimViews,\n          },\n        }),\n      });\n    case actions.ASSET_VIEWS_UPDATE:\n      return Object.assign({}, state, {\n        assetList: Object.assign({}, state.assetList, {\n          [action.data.id]: {\n            ...state.assetList[action.data.id],\n            claimViews: action.data.claimViews,\n          },\n        }),\n      });\n    case actions.ASSET_REMOVE:\n      const claim = action.data;\n      const newAssetList = state.assetList;\n      delete newAssetList[`a#${claim.name}#${claim.claimId}`];\n\n      const channelId = `c#${claim.channelName}#${claim.certificateId}`;\n      const channelClaims = state.channelList[channelId].claimsData.claims;\n      const newClaimsData = channelClaims.filter(c => c.claimId !== claim.claimId);\n\n      return {\n        ...state,\n        assetList: newAssetList,\n        channelList: {\n          ...state.channelList,\n          [channelId]: {\n            ...state.channelList[channelId],\n            claimsData: {\n              ...state.channelList[channelId].claimsData,\n              claims: newClaimsData,\n            },\n          },\n        },\n      };\n    case actions.ASSET_UPDATE_CLAIMDATA:\n      return {\n        ...state,\n        assetList: {\n          ...state.assetList,\n          [action.data.id]: {\n            ...state.assetList[action.data.id],\n            claimData: {\n              ...state.assetList[action.data.id].claimData,\n              ...action.data.claimData,\n            },\n          },\n        },\n      };\n    // channel data\n    case actions.CHANNEL_ADD:\n      return Object.assign({}, state, {\n        channelList: Object.assign({}, state.channelList, {\n          [action.data.id]: {\n            name: action.data.name,\n            longId: action.data.longId,\n            shortId: action.data.shortId,\n            claimsData: action.data.claimsData,\n          },\n        }),\n      });\n    case actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED:\n      return Object.assign({}, state, {\n        channelList: Object.assign({}, state.channelList, {\n          [action.data.channelListId]: Object.assign(\n            {},\n            state.channelList[action.data.channelListId],\n            {\n              claimsData: action.data.claimsData,\n            }\n          ),\n        }),\n      });\n    // display an asset\n    case actions.FILE_AVAILABILITY_UPDATE:\n      return Object.assign({}, state, {\n        displayAsset: Object.assign({}, state.displayAsset, {\n          status: action.data,\n        }),\n      });\n    case actions.DISPLAY_ASSET_ERROR:\n      return Object.assign({}, state, {\n        displayAsset: Object.assign({}, state.displayAsset, {\n          error: action.data,\n          status: ERROR,\n        }),\n      });\n    case actions.TOGGLE_DETAILS_EXPANDED:\n      return {\n        ...state,\n        detailsExpanded: action.data,\n      };\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "client/src/reducers/site.js",
    "content": "import siteConfig from '@config/siteConfig.json';\n\nlet initialState = {\n  description        : 'default description',\n  googleAnalyticsId  : 'default google id',\n  host               : 'default host',\n  title              : 'default title',\n  twitter            : 'default twitter',\n  defaultDescription : 'default description',\n  defaultThumbnail   : 'default thumbnail',\n  closedRegistration : false,\n  serveOnlyApproved  : false,\n  publishOnlyApproved: false,\n  approvedChannels   : [],\n\n};\n\nif (siteConfig) {\n  const {\n    analytics: {\n      googleId: googleAnalyticsId,\n    },\n    assetDefaults: {\n      thumbnail: defaultThumbnail,\n      description: defaultDescription,\n    },\n    details: {\n      description,\n      host,\n      title,\n      twitter,\n    },\n    publishing: {\n      closedRegistration,\n      serveOnlyApproved,\n      publishOnlyApproved,\n      approvedChannels,\n    },\n  } = siteConfig;\n\n  initialState = {\n    description,\n    googleAnalyticsId,\n    host,\n    title,\n    twitter,\n    defaultDescription,\n    defaultThumbnail,\n    closedRegistration,\n    serveOnlyApproved,\n    publishOnlyApproved,\n    approvedChannels,\n  };\n}\n\nexport default function (state = initialState, action) {\n  switch (action.type) {\n    default:\n      return state;\n  }\n};\n"
  },
  {
    "path": "client/src/sagas/abandon.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/publish_action_types';\nimport * as publishStates from '../constants/publish_claim_states';\nimport { updatePublishStatus, clearFile } from '../actions/publish';\nimport { removeAsset } from '../actions/show';\nimport { doAbandonClaim } from '../api/assetApi';\n\nfunction* abandonClaim(action) {\n  const { claimData, history } = action.data;\n  const { outpoint } = claimData;\n\n  const confirm = window.confirm(\n    'Are you sure you want to abandon this claim? This action cannot be undone.'\n  );\n  if (!confirm) return;\n\n  yield put(updatePublishStatus(publishStates.ABANDONING, 'Your claim is being abandoned...'));\n\n  try {\n    yield call(doAbandonClaim, outpoint);\n  } catch (error) {\n    return console.log('abandon error:', error.message);\n  }\n\n  yield put(clearFile());\n  yield put(removeAsset(claimData));\n  return history.push('/');\n}\n\nexport function* watchAbandonClaim() {\n  yield takeLatest(actions.ABANDON_CLAIM, abandonClaim);\n}\n"
  },
  {
    "path": "client/src/sagas/checkForLoggedInChannel.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport { CHANNEL_LOGIN_CHECK } from '../constants/channel_action_types';\nimport { checkForLoggedInChannelApi } from '../api/authApi.js';\nimport { updateSelectedChannel } from '../actions/publish';\nimport { updateLoggedInChannel } from '../actions/channel';\n\nfunction * checkForLoggedInChannelSaga () {\n  let response;\n  try {\n    response = yield call(checkForLoggedInChannelApi);\n  } catch (error) {\n    return console.log(error);\n  }\n  if (response.data) {\n    const { data: { channelName, shortChannelId, channelClaimId } } = response;\n    yield put(updateSelectedChannel(channelName));\n    yield put(updateLoggedInChannel(channelName, shortChannelId, channelClaimId));\n  }\n}\n\nexport function * watchChannelLoginCheck () {\n  yield takeLatest(CHANNEL_LOGIN_CHECK, checkForLoggedInChannelSaga);\n}\n"
  },
  {
    "path": "client/src/sagas/createChannel.js",
    "content": "import { call, put, select, takeLatest } from 'redux-saga/effects';\nimport { CHANNEL_CREATE } from '../constants/channel_create_action_types';\nimport { selectChannelCreateState } from '../selectors/channelCreate';\nimport {\n  validateCreateChannelNameInput,\n  validateCreateChannelPasswordInput,\n} from '../utils/validate';\nimport {\n  updateChannelCreateName,\n  updateChannelCreatePassword,\n  updateChannelCreateStatus,\n} from '../actions/channelCreate';\nimport { makeCreateChannelRequest } from '../api/channelApi';\nimport { updateLoggedInChannel } from '../actions/channel';\nimport {updateSelectedChannel} from '../actions/publish';\n\nfunction * createChannel () {\n  const { name, password } = yield select(selectChannelCreateState);\n  // validate the name\n  try {\n    validateCreateChannelNameInput(name);\n  } catch (error) {\n    return yield put(updateChannelCreateName('error', error.message));\n  }\n  // validate the password\n  try {\n    validateCreateChannelPasswordInput(password);\n  } catch (error) {\n    return yield put(updateChannelCreatePassword('error', error.message));\n  }\n  // update status\n  yield put(updateChannelCreateStatus('We are publishing your new channel.  Sit tight...'));\n  // make the create channel request\n  let channelName, shortChannelId, channelClaimId;\n  try {\n    ({ channelName, shortChannelId, channelClaimId } = yield call(makeCreateChannelRequest, name.value, password.value));\n  } catch (error) {\n    return yield put(updateChannelCreateStatus(error.message));\n  }\n  yield put(updateChannelCreateStatus(null));\n  yield put(updateLoggedInChannel(channelName, shortChannelId, channelClaimId));\n  yield put(updateSelectedChannel(channelName));\n}\n\nexport function * watchChannelCreate () {\n  yield takeLatest(CHANNEL_CREATE, createChannel);\n}\n"
  },
  {
    "path": "client/src/sagas/file.js",
    "content": "import {call, put, select, takeLatest} from 'redux-saga/effects';\nimport * as actions from '../constants/show_action_types';\nimport { updateFileAvailability, updateDisplayAssetError } from '../actions/show';\nimport { UNAVAILABLE, AVAILABLE } from '../constants/asset_display_states';\nimport { checkFileAvailability, triggerClaimGet } from '../api/fileApi';\nimport { selectSiteHost } from '../selectors/site';\n\nfunction * retrieveFile (action) {\n  const name = action.data.name;\n  const claimId = action.data.claim_id || action.data.claimId;\n  const host = yield select(selectSiteHost);\n  // see if the file is available\n  let isAvailable;\n  try {\n    ({ data: isAvailable } = yield call(checkFileAvailability, claimId, host, name));\n  } catch (error) {\n    return yield put(updateDisplayAssetError(error.message));\n  };\n  if (isAvailable) {\n    yield put(updateDisplayAssetError(null));\n    return yield put(updateFileAvailability(AVAILABLE));\n  }\n  yield put(updateFileAvailability(UNAVAILABLE));\n  // initiate get request for the file\n  try {\n    yield call(triggerClaimGet, claimId, host, name);\n  } catch (error) {\n    return yield put(updateDisplayAssetError(error.message));\n  };\n  yield put(updateFileAvailability(AVAILABLE));\n};\n\nexport function * watchFileIsRequested () {\n  yield takeLatest(actions.FILE_REQUESTED, retrieveFile);\n};\n"
  },
  {
    "path": "client/src/sagas/index.js",
    "content": "import { rootSaga } from './rootSaga';\nimport { handleShowPageUri } from './show_uri';\n\nexport default {\n  rootSaga,\n  handleShowPageUri,\n};\n"
  },
  {
    "path": "client/src/sagas/logoutChannel.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport { CHANNEL_LOGOUT } from '../constants/channel_action_types';\nimport { channelLogoutApi } from '../api/authApi.js';\nimport { updateLoggedInChannel } from '../actions/channel';\n\nfunction * logoutChannelSaga () {\n  try {\n    yield call(channelLogoutApi);\n  } catch (error) {\n    return console.log(error);\n  }\n  yield put(updateLoggedInChannel(null, null, null));\n}\n\nexport function * watchChannelLogout () {\n  yield takeLatest(CHANNEL_LOGOUT, logoutChannelSaga);\n}\n"
  },
  {
    "path": "client/src/sagas/publish.js",
    "content": "import { call, put, select, take, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/publish_action_types';\nimport * as publishStates from '../constants/publish_claim_states';\nimport { updateError, updatePublishStatus, clearFile } from '../actions/publish';\nimport { selectPublishState } from '../selectors/publish';\nimport { selectChannelState } from '../selectors/channel';\nimport { selectSiteState } from '../selectors/site';\nimport { selectShowState, selectAsset } from '../selectors/show';\nimport { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';\nimport { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';\nimport { makePublishRequestChannel } from '../channels/publish';\n// yep\nfunction* publishFile(action) {\n  const { history } = action.data;\n  const publishState = yield select(selectPublishState);\n  const {\n    publishInChannel,\n    selectedChannel,\n    file,\n    claim,\n    metadata,\n    thumbnailChannel,\n    thumbnailChannelId,\n    thumbnail,\n    isUpdate,\n    error: publishToolErrors,\n  } = publishState;\n  const { loggedInChannel } = yield select(selectChannelState);\n  const { host } = yield select(selectSiteState);\n\n  let show, asset;\n  if (isUpdate) {\n    show = yield select(selectShowState);\n    asset = selectAsset(show);\n  }\n  // validate the channel selection\n  try {\n    validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);\n  } catch (error) {\n    return yield put(updateError('channel', error.message));\n  }\n  // validate publish parameters\n  try {\n    validateNoPublishErrors(publishToolErrors);\n  } catch (error) {\n    return console.log('publish error:', error.message);\n  }\n\n  let publishMetadata, publishFormData, publishChannel;\n  // create metadata\n  publishMetadata = createPublishMetadata(\n    isUpdate ? asset.name : claim,\n    isUpdate ? { type: asset.claimData.contentType } : file,\n    metadata,\n    publishInChannel,\n    selectedChannel\n  );\n  if (isUpdate) {\n    publishMetadata['channelName'] = asset.claimData.channelName;\n  }\n  if (thumbnail) {\n    // add thumbnail to publish metadata\n    publishMetadata['thumbnail'] = createThumbnailUrl(\n      thumbnailChannel,\n      thumbnailChannelId,\n      claim,\n      host\n    );\n  }\n  // create form data for main publish\n  publishFormData = createPublishFormData(file, thumbnail, publishMetadata);\n  // make the publish request\n  publishChannel = yield call(makePublishRequestChannel, publishFormData, isUpdate);\n\n  while (true) {\n    const { loadStart, progress, load, success, error: publishError } = yield take(publishChannel);\n    if (publishError) {\n      return yield put(updatePublishStatus(publishStates.FAILED, publishError.message));\n    }\n    if (success) {\n      yield put(clearFile());\n      if (isUpdate) {\n        yield put({\n          type: 'ASSET_UPDATE_CLAIMDATA',\n          data: {\n            id: `a#${success.data.name}#${success.data.claimId}`,\n            claimData: success.data.claimData,\n          },\n        });\n      }\n      if (success.data.claimId) {\n        return history.push(success.data.pushTo);\n      } else {\n        // this returns to the homepage, needs work\n        return yield put(updatePublishStatus(publishStates.FAILED, 'ERROR'));\n      }\n    }\n    if (loadStart) {\n      yield put(updatePublishStatus(publishStates.LOAD_START, null));\n    }\n    if (progress) {\n      yield put(updatePublishStatus(publishStates.LOADING, `${progress}%`));\n    }\n    if (load) {\n      yield put(updatePublishStatus(publishStates.PUBLISHING, null));\n    }\n  }\n}\n\nexport function* watchPublishStart() {\n  yield takeLatest(actions.PUBLISH_START, publishFile);\n}\n"
  },
  {
    "path": "client/src/sagas/rootSaga.js",
    "content": "import { all } from 'redux-saga/effects';\nimport { watchHandleShowPageUri, watchHandleShowHomepage } from './show_uri';\nimport { watchNewAssetRequest, watchUpdateAssetViews } from './show_asset';\nimport { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';\nimport { watchNewSpecialAssetRequest } from './show_special';\nimport { watchFileIsRequested } from './file';\nimport { watchPublishStart } from './publish';\nimport { watchUpdateClaimAvailability } from './updateClaimAvailability';\nimport { watchUpdateChannelAvailability } from './updateChannelAvailability';\nimport { watchChannelCreate } from './createChannel';\nimport { watchChannelLoginCheck } from './checkForLoggedInChannel';\nimport { watchChannelLogout } from './logoutChannel';\nimport { watchAbandonClaim } from './abandon';\n\nexport function * rootSaga () {\n  yield all([\n    watchHandleShowPageUri(),\n    watchHandleShowHomepage(),\n    watchNewAssetRequest(),\n    watchNewChannelRequest(),\n    watchNewSpecialAssetRequest(),\n    watchUpdateChannelClaims(),\n    watchFileIsRequested(),\n    watchPublishStart(),\n    watchUpdateClaimAvailability(),\n    watchUpdateChannelAvailability(),\n    watchChannelCreate(),\n    watchChannelLoginCheck(),\n    watchChannelLogout(),\n    watchUpdateAssetViews(),\n    watchAbandonClaim(),\n  ]);\n}\n"
  },
  {
    "path": "client/src/sagas/show_asset.js",
    "content": "import { call, put, select, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/show_action_types';\nimport * as channelActions from '../constants/channel_action_types';\nimport {\n  addRequestToRequestList,\n  onRequestError,\n  onRequestUpdate,\n  addAssetToAssetList,\n  updateAssetViewsInList,\n} from '../actions/show';\nimport { getLongClaimId, getShortId, getClaimData, getClaimViews } from '../api/assetApi';\nimport { selectChannelState } from '../selectors/channel';\nimport { selectShowState } from '../selectors/show';\nimport { selectSiteHost } from '../selectors/site';\n\nexport function * newAssetRequest (action) {\n  const { requestType, requestId, name, modifier } = action.data;\n  // put an action to update the request in redux\n  yield put(onRequestUpdate(requestType, requestId));\n  // is this an existing request?\n  // If this uri is in the request list, it's already been fetched\n  const state = yield select(selectShowState);\n  const host = yield select(selectSiteHost);\n  if (state.requestList[requestId]) {\n    return null;\n  }\n  // get long id && add request to request list\n  let longId;\n  try {\n    ({data: longId} = yield call(getLongClaimId, host, name, modifier));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n  const assetKey = `a#${name}#${longId}`;\n  yield put(addRequestToRequestList(requestId, null, assetKey));\n  // is this an existing asset?\n  // If this asset is in the asset list, it's already been fetched\n  if (state.assetList[assetKey]) {\n    return null;\n  }\n  // get short Id\n  let shortId;\n  try {\n    ({data: shortId} = yield call(getShortId, host, name, longId));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n\n  // get asset claim data\n  let claimData;\n  let claimViews = null;\n\n  try {\n    ({data: claimData} = yield call(getClaimData, host, name, longId));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n\n  try {\n    const { loggedInChannel } = yield select(selectChannelState);\n\n    if (loggedInChannel && loggedInChannel.longId) {\n      const {\n        data: claimViewData,\n      } = yield call(getClaimViews, longId);\n\n      claimViews = claimViewData[longId] || 0;\n    }\n  } catch (error) { }\n\n  // add asset to asset list\n  yield put(addAssetToAssetList(assetKey, null, name, longId, shortId, claimData, claimViews));\n  // clear any errors in request error\n  yield put(onRequestError(null));\n};\n\nexport function * updateAssetViews (action) {\n  // update each loaded claim that's in the loggedInChannel\n  try {\n    const showState = yield select(selectShowState);\n    const { data: loggedInChannel } = action;\n\n    const channelId = loggedInChannel.longId;\n\n    for (let key in showState.assetList) {\n      let asset = showState.assetList[key];\n\n      if (asset.claimData && asset.claimData.certificateId === channelId) {\n        const longId = asset.claimId;\n        const assetKey = `a#${asset.name}#${longId}`;\n\n        let claimViews = null;\n\n        if (longId) {\n          const {\n            data: claimViewData,\n          } = yield call(getClaimViews, longId);\n\n          claimViews = claimViewData[longId] || 0;\n        }\n\n        yield put(updateAssetViewsInList(assetKey, longId, claimViews));\n      }\n    }\n  } catch (error) {\n    console.log(error);\n  }\n};\n\nexport function * watchUpdateAssetViews (action) {\n  yield takeLatest(channelActions.CHANNEL_UPDATE, updateAssetViews);\n};\n\nexport function * watchNewAssetRequest () {\n  yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);\n};\n"
  },
  {
    "path": "client/src/sagas/show_channel.js",
    "content": "import {call, put, select, takeLatest} from 'redux-saga/effects';\nimport * as actions from '../constants/show_action_types';\nimport { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from '../actions/show';\nimport { getChannelClaims, getChannelData } from '../api/channelApi';\nimport { selectShowState } from '../selectors/show';\nimport { selectSiteHost } from '../selectors/site';\n\nexport function * newChannelRequest (action) {\n  const { requestType, requestId, channelName, channelId } = action.data;\n  let claimsData;\n  // put an action to update the request in redux\n  yield put(onRequestUpdate(requestType, requestId));\n  // is this an existing request?\n  // If this uri is in the request list, it's already been fetched\n  const state = yield select(selectShowState);\n  const host = yield select(selectSiteHost);\n  if (state.requestList[requestId]) {\n    return null;\n  }\n  // get channel long id\n  let longId, shortId;\n  try {\n    ({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, host, channelName, channelId));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n  // store the request in the channel requests list\n  const channelKey = `c#${channelName}#${longId}`;\n  yield put(addRequestToRequestList(requestId, null, channelKey));\n\n  // If this channel is in the channel list, it's already been fetched\n  if (state.channelList[channelKey]) {\n    return null;\n  }\n  // get channel claims data\n  try {\n    ({ data: claimsData } = yield call(getChannelClaims, host, channelName, longId, 1));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n\n  // store the channel data in the channel list\n  yield put(addNewChannelToChannelList(channelKey, channelName, shortId, longId, claimsData));\n\n  // clear any request errors\n  yield put(onRequestError(null));\n}\n\nexport function * watchNewChannelRequest () {\n  yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);\n}\n\nfunction * getNewClaimsAndUpdateChannel (action) {\n  const { channelKey, name, longId, page } = action.data;\n  const host = yield select(selectSiteHost);\n  let claimsData;\n  try {\n    ({ data: claimsData } = yield call(getChannelClaims, host, name, longId, page));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n  yield put(updateChannelClaims(channelKey, claimsData));\n}\n\nexport function * watchUpdateChannelClaims () {\n  yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel);\n}\n"
  },
  {
    "path": "client/src/sagas/show_special.js",
    "content": "import {call, put, select, takeLatest} from 'redux-saga/effects';\nimport * as actions from '../constants/show_action_types';\nimport { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from '../actions/show';\n// import { getChannelClaims, getChannelData } from '../api/channelApi';\nimport { getSpecialAssetClaims } from '../api/specialAssetApi';\nimport { selectShowState } from '../selectors/show';\nimport { selectSiteHost } from '../selectors/site';\n\nexport function * newSpecialAssetRequest (action) {\n  const { requestType, requestId, name } = action.data;\n  let claimsData;\n  // put an action to update the request in redux\n  yield put(onRequestUpdate(requestType, requestId));\n  // is this an existing request?\n  // If this uri is in the request list, it's already been fetched\n  const state = yield select(selectShowState);\n  const host = yield select(selectSiteHost);\n  if (state.requestList[requestId]) {\n    return null;\n  }\n\n  // store the request in the channel requests list\n  const channelKey = `sar#${name}`;\n  yield put(addRequestToRequestList(requestId, null, channelKey));\n\n  // If this channel is in the channel list, it's already been fetched\n  if (state.channelList[channelKey]) {\n    return null;\n  }\n  // get channel claims data\n  try {\n    ({ data: claimsData } = yield call(getSpecialAssetClaims, host, name, 1));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n\n  // store the channel data in the channel list\n  yield put(addNewChannelToChannelList(channelKey, name, null, null, claimsData));\n\n  // clear any request errors\n  yield put(onRequestError(null));\n}\n\nexport function * watchNewSpecialAssetRequest () {\n  yield takeLatest(actions.SPECIAL_ASSET_REQUEST_NEW, newSpecialAssetRequest);\n}\n"
  },
  {
    "path": "client/src/sagas/show_uri.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/show_action_types';\nimport {\n  onRequestError,\n  onNewChannelRequest,\n  onNewAssetRequest,\n  onNewSpecialAssetRequest,\n} from '../actions/show';\nimport { newAssetRequest } from '../sagas/show_asset';\nimport { newChannelRequest } from '../sagas/show_channel';\nimport { newSpecialAssetRequest } from '../sagas/show_special';\nimport lbryUri from '../../../utils/lbryUri';\n\nfunction * parseAndUpdateIdentifierAndClaim (modifier, claim) {\n  // this is a request for an asset\n  // claim will be an asset claim\n  // the identifier could be a channel or a claim id\n  let isChannel, channelName, channelClaimId, claimId, claimName, extension;\n  try {\n    ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(modifier));\n    ({ claimName, extension } = lbryUri.parseClaim(claim));\n  } catch (error) {\n    return yield put(onRequestError(error.message));\n  }\n  // trigger an new action to update the store\n  if (isChannel) {\n    return yield call(newAssetRequest, onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));\n  };\n  yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));\n}\n\nfunction * parseAndUpdateClaimOnly (claim) {\n  if (/^special:/.test(claim) === true) {\n    const assetName = /special:(.*)/.exec(claim)[1];\n    return yield call(newSpecialAssetRequest, onNewSpecialAssetRequest(assetName));\n  } else {\n    // this could be a request for an asset or a channel page\n    // claim could be an asset claim or a channel claim\n    let isChannel, channelName, channelClaimId;\n    try {\n      ({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(claim));\n    } catch (error) {\n      return yield put(onRequestError(error.message));\n    }\n    // trigger an new action to update the store\n    // return early if this request is for a channel\n    if (isChannel) {\n      return yield call(newChannelRequest, onNewChannelRequest(channelName, channelClaimId));\n    }\n    // if not for a channel, parse the claim request\n    let claimName, extension;\n    try {\n      ({claimName, extension} = lbryUri.parseClaim(claim));\n    } catch (error) {\n      return yield put(onRequestError(error.message));\n    }\n    yield call(newAssetRequest, onNewAssetRequest(claimName, null, null, null, extension));\n  }\n}\n\nexport function * handleShowPageUri (action) {\n  const { identifier, claim } = action.data;\n  if (identifier) {\n    return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim);\n  } else if (claim) {\n    yield call(parseAndUpdateClaimOnly, claim);\n  }\n};\n\nexport function * handleShowPageHomepage (action) {\n  const { identifier, claim } = action.data;\n  if (identifier) {\n    return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim);\n  } else if (claim) {\n    yield call(parseAndUpdateClaimOnly, claim);\n  }\n};\n\nexport function * watchHandleShowPageUri () {\n  yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri);\n};\n\nexport function * watchHandleShowHomepage () {\n  yield takeLatest(actions.HANDLE_SHOW_HOMEPAGE, handleShowPageHomepage);\n};\n"
  },
  {
    "path": "client/src/sagas/updateChannelAvailability.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/channel_create_action_types';\nimport { checkChannelAvailability } from '../api/channelApi';\nimport { updateChannelCreateName } from '../actions/channelCreate';\n\nfunction * updateChannelAvailability ({data}) {\n  let isAvailable, message;\n  try {\n    ({ data: isAvailable, message } = yield call(checkChannelAvailability, data));\n    console.log('isAvailable:', isAvailable, 'message:', message);\n  } catch (error) {\n    console.log('updateClaimAvailability error');\n  }\n  if (!isAvailable) {\n    return yield put(updateChannelCreateName('error', message));\n  }\n  yield put(updateChannelCreateName('error', null));\n}\n\nexport function * watchUpdateChannelAvailability () {\n  yield takeLatest(actions.CHANNEL_AVAILABILITY, updateChannelAvailability);\n}\n"
  },
  {
    "path": "client/src/sagas/updateClaimAvailability.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\nimport * as actions from '../constants/publish_action_types';\nimport { updateError } from '../actions/publish';\nimport { checkClaimAvailability } from '../api/assetApi';\n\nfunction * updateClaimAvailability ({data}) {\n  let isAvailable, message;\n  try {\n    ({ data: isAvailable, message } = yield call(checkClaimAvailability, data));\n  } catch (error) {\n    return console.log(error);\n  }\n  if (!isAvailable) {\n    return yield put(updateError('url', message));\n  }\n  yield put(updateError('url', null));\n}\n\nexport function * watchUpdateClaimAvailability () {\n  yield takeLatest(actions.CLAIM_AVAILABILITY, updateClaimAvailability);\n}\n"
  },
  {
    "path": "client/src/selectors/channel.js",
    "content": "export const selectChannelState = (state) => {\n  return state.channel;\n};\n"
  },
  {
    "path": "client/src/selectors/channelCreate.js",
    "content": "export const selectChannelCreateState = (state) => {\n  return state.channelCreate;\n};\n"
  },
  {
    "path": "client/src/selectors/publish.js",
    "content": "export const selectPublishState = (state) => {\n  return state.publish;\n};\n"
  },
  {
    "path": "client/src/selectors/show.js",
    "content": "export const selectAsset = show => {\n  const requestId = show.request.id;\n  let asset;\n  const request = show.requestList[requestId] || null;\n  const assetList = show.assetList;\n  if (request && assetList) {\n    const assetKey = request.key; // note: just store this in the request\n    asset = assetList[assetKey] || null;\n  }\n  return asset;\n};\n\nexport const selectShowState = state => {\n  return state.show;\n};\n\nexport const selectDetailsExpanded = state => {\n  return state.show.detailsExpanded;\n};\n"
  },
  {
    "path": "client/src/selectors/site.js",
    "content": "export const selectSiteState = (state) => {\n  return state.site;\n};\n\nexport const selectSiteHost = (state) => {\n  return state.site.host;\n};\n"
  },
  {
    "path": "client/src/utils/createAssetMetaTags.js",
    "content": "import siteConfig from '@config/siteConfig.json';\nimport determineContentTypeFromExtension from './determineContentTypeFromExtension';\nimport createMetaTagsArray from './createMetaTagsArray';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\n\nconst {\n  details: { host, title: siteTitle, twitter },\n  assetDefaults: { description: defaultDescription, thumbnail: defaultThumbnail },\n} = siteConfig;\n\nconst VIDEO = 'VIDEO';\nconst IMAGE = 'IMAGE';\nconst GIF = 'GIF';\nconst TEXT = 'TEXT';\n\nconst determineMediaType = contentType => {\n  switch (contentType) {\n    case 'image/jpg':\n    case 'image/jpeg':\n    case 'image/png':\n    case 'image/svg+xml':\n      return IMAGE;\n    case 'image/gif':\n      return GIF;\n    case 'video/mp4':\n    case 'video/webm':\n      return VIDEO;\n    case 'text/markdown':\n    case 'text/plain':\n      return TEXT;\n    default:\n      return '';\n  }\n};\n\nconst createAssetMetaTags = asset => {\n  const { claimData } = asset;\n  const { contentType } = claimData;\n  const canonicalLink = createCanonicalLink({\n    asset: { ...asset.claimData, shortId: asset.shortId },\n  });\n  const showUrl = `${host}${canonicalLink}`;\n  const serveUrl = `${showUrl}.${claimData.fileExt}`;\n\n  const ogTitle = claimData.title || claimData.name;\n  const ogDescription = claimData.description || defaultDescription;\n  const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail);\n  const ogThumbnail = claimData.thumbnail || defaultThumbnail;\n\n  // {property: 'og:title'] = ogTitle},\n  const metaTags = {\n    'og:title': ogTitle,\n    'twitter:title': ogTitle,\n    'og:description': ogDescription,\n    'twitter:description': ogDescription,\n    'og:url': showUrl,\n    'og:site_name': siteTitle,\n    'twitter:site': twitter,\n    'fb:app_id': '1371961932852223',\n  };\n  if (determineMediaType(contentType) === VIDEO) {\n    const videoEmbedUrl = `${host}/video-embed${canonicalLink}`;\n    // card type tags\n    metaTags['og:type'] = 'video.other';\n    metaTags['twitter:card'] = 'player';\n    metaTags['twitter:player'] = videoEmbedUrl;\n    metaTags['twitter:player:width'] = 600;\n    metaTags['twitter:text:player_width'] = 600;\n    metaTags['twitter:player:height'] = 350;\n    metaTags['twitter:player:stream'] = serveUrl;\n    metaTags['twitter:player:stream:content_type'] = contentType;\n    // video tags\n    metaTags['og:video'] = serveUrl;\n    metaTags['og:video:secure_url'] = serveUrl;\n    metaTags['og:video:type'] = contentType;\n    // image tags\n    metaTags['og:image'] = ogThumbnail;\n    metaTags['og:image:width'] = 600;\n    metaTags['og:image:height'] = 315;\n    metaTags['og:image:type'] = ogThumbnailContentType;\n    metaTags['twitter:image'] = ogThumbnail;\n  } else {\n    // card type tags\n    metaTags['og:type'] = 'article';\n    metaTags['twitter:card'] = 'summary_large_image';\n    // image tags\n    metaTags['og:image'] = serveUrl;\n    metaTags['og:image'] = serveUrl;\n    metaTags['og:image:type'] = contentType;\n    metaTags['twitter:image'] = serveUrl;\n  }\n  return createMetaTagsArray(metaTags);\n};\n\nexport default createAssetMetaTags;\n"
  },
  {
    "path": "client/src/utils/createBasicMetaTags.js",
    "content": "import siteConfig from '@config/siteConfig.json';\nimport determineContentTypeFromExtension from './determineContentTypeFromExtension.js';\nimport createMetaTagsArray from './createMetaTagsArray';\n\nconst {\n  details: {\n    description,\n    host,\n    title,\n    twitter,\n  },\n  assetDefaults: {\n    thumbnail,\n  },\n} = siteConfig;\n\nconst createBasicMetaTags = () => {\n  const metaTags = {\n    // page details\n    'og:title'           : title,\n    'twitter:title'      : title,\n    'og:description'     : description,\n    'twitter:description': description,\n    // url\n    'og:url'             : host,\n    // site id\n    'og:site_name'       : title,\n    'twitter:site'       : twitter,\n    'fb:app_id'          : '1371961932852223',\n    // card type\n    'og:type'            : 'article',\n    'twitter:card'       : 'summary_large_image',\n    // image\n    'og:image'           : thumbnail,\n    'og:image:width'     : 600,\n    'og:image:height'    : 315,\n    'og:image:type'      : determineContentTypeFromExtension(thumbnail),\n    'twitter:image'      : thumbnail,\n    'twitter:image:alt'  : 'Spee.ch Logo',\n  };\n  return createMetaTagsArray(metaTags);\n};\n\nexport default createBasicMetaTags;\n"
  },
  {
    "path": "client/src/utils/createChannelMetaTags.js",
    "content": "import siteConfig from '@config/siteConfig.json';\nimport determineContentTypeFromExtension from './determineContentTypeFromExtension';\nimport createMetaTagsArray from './createMetaTagsArray';\nimport createCanonicalLink from '@globalutils/createCanonicalLink';\n\nconst {\n  details: {\n    host,\n    title: siteTitle,\n    twitter,\n  },\n  assetDefaults: {\n    thumbnail: defaultThumbnail,\n  },\n} = siteConfig;\n\nexport const createChannelMetaTags = (channel) => {\n  const { name, shortId } = channel;\n  const metaTags = {\n    // page detail tags\n    'og:title'           : `${name} on ${siteTitle}`,\n    'twitter:title'      : `${name} on ${siteTitle}`,\n    'og:description'     : `${name}, a channel on ${siteTitle}`,\n    'twitter:description': `${name}, a channel on ${siteTitle}`,\n    // url\n    'og:url'             : `${host}/${createCanonicalLink({ channel })}`,\n    // site info\n    'og:site_name'       : siteTitle,\n    'twitter:site'       : twitter,\n    'fb:app_id'          : '1371961932852223',\n    // card type tags\n    'og:type'            : 'article',\n    'twitter:card'       : 'summary_large_image',\n    // image tags\n    'og:image'           : defaultThumbnail,\n    'og:image:width'     : 600,\n    'og:image:height'    : 315,\n    'og:image:type'      : determineContentTypeFromExtension(defaultThumbnail),\n    'twitter:image'      : defaultThumbnail,\n    'twitter:image:alt'  : 'Spee.ch Logo',\n  };\n  return createMetaTagsArray(metaTags);\n};\n\nexport default createChannelMetaTags;\n"
  },
  {
    "path": "client/src/utils/createGroupedList.js",
    "content": "export const createGroupedList = (list, size) => {\n  if (!size) {\n    throw new Error('no size provided to createGroupedList');\n  }\n\n  if (!list) {\n    throw new Error('no list provided to createGroupedList');\n  }\n  let groupedList = [];\n  for (let i = 0; i < list.length; i = i + size) {\n    let group = [];\n    for (let j = i; j < (i + size); j++) {\n      group.push(list[j]);\n    }\n    groupedList.push(group);\n  }\n  return groupedList;\n};\n"
  },
  {
    "path": "client/src/utils/createMetaTags.js",
    "content": "import createAssetMetaTags from './createAssetMetaTags';\nimport createChannelMetaTags from './createChannelMetaTags.js';\nimport createBasicMetaTags from './createBasicMetaTags.js';\n\nconst createMetaTags = ({ asset, channel }) => {\n  if (asset) {\n    return createAssetMetaTags(asset);\n  }\n  if (channel) {\n    return createChannelMetaTags(channel);\n  }\n  return createBasicMetaTags();\n};\n\nexport default createMetaTags;\n"
  },
  {
    "path": "client/src/utils/createMetaTagsArray.js",
    "content": "const createMetaTagsArray = (metaTagsObject) => {\n  let metaTagsArray = [];\n  for (let key in metaTagsObject) {\n    if (metaTagsObject.hasOwnProperty(key)) {\n      metaTagsArray.push({\n        property: key,\n        content : metaTagsObject[key],\n      });\n    }\n  }\n  return metaTagsArray;\n};\n\nexport default createMetaTagsArray;\n"
  },
  {
    "path": "client/src/utils/createPageTitle.js",
    "content": "import siteConfig from '@config/siteConfig.json';\n\nconst {\n  details: {\n    title: siteTitle,\n  },\n} = siteConfig;\n\nconst createPageTitle = (pageTitle) => {\n  if (!pageTitle) {\n    return `${siteTitle}`;\n  }\n  return `${siteTitle} - ${pageTitle}`;\n};\n\nexport default createPageTitle;\n"
  },
  {
    "path": "client/src/utils/createPermanentURI.js",
    "content": "/*\n{ channelName, certificateId, name, claimId } = { claimData } = asset\n\npermanentUrl for a channel\n@channelName#certificateId\n\npermanentUrl for an asset in a channel\n@channelName#certificateId/name\n\npermanentUrl for an asset published anonymously\nname#claimId\n*/\n\nexport const createPermanentURI = asset => {\n  let channelName, certificateId, name, claimId;\n  if (asset.claimData) {\n    ({ channelName, certificateId, name, claimId } = asset.claimData);\n  }\n  else return 'Error: unknown asset at createPermanentURI.js';\n  if (channelName) {\n    return `${channelName}#${certificateId}/${name}`;\n  }\n  return `${name}#${claimId}`;\n};\n"
  },
  {
    "path": "client/src/utils/determineContentTypeFromExtension.js",
    "content": "const determineContentTypeFromExtension = thumbnail => {\n  if (thumbnail) {\n    const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.'));\n    switch (fileExt) {\n      case 'jpeg':\n      case 'jpg':\n        return 'image/jpeg';\n      case 'png':\n        return 'image/png';\n      case 'gif':\n        return 'image/gif';\n      case 'mp4':\n        return 'video/mp4';\n      case 'svg':\n        return 'image/svg+xml';\n      case 'md':\n      case 'markdown':\n        return 'text/markdown';\n      default:\n        return '';\n    }\n  }\n  return '';\n};\n\nexport default determineContentTypeFromExtension;\n"
  },
  {
    "path": "client/src/utils/dynamicImport.js",
    "content": "function getDeepestChildValue (parent, childrenKeys) {\n  if (!parent[childrenKeys[0]]) {\n    return null;\n  }\n  let childKey = childrenKeys.shift();\n  let child = parent[childKey];\n  if (childrenKeys.length >= 1) {\n    return getDeepestChildValue(child, childrenKeys);\n  }\n  return child;\n}\n\nexport const dynamicImport = (filePath, customViews) => {\n  console.log('looking for', filePath, 'in', customViews);\n  // validate inputs\n  if (!filePath) {\n    throw new Error('no file path provided to dynamicImport()');\n  }\n  if (typeof filePath !== 'string') {\n    throw new Error('file path provided to dynamicImport() must be a string');\n  }\n  if (!customViews) {\n    return null;\n  }\n  // split out the file folders; filter out any empty or white-space-only strings\n  const folders = filePath.split('/').filter(folderName => folderName.replace(/\\s/g, '').length);\n  // check for the component corresponding to file path in the site config object\n  const component = getDeepestChildValue(customViews, folders);\n  if (component) {\n    console.log('found custom component for', filePath);\n    return component;\n  } else {\n    console.log('no custom component for', filePath);\n    return null;\n  }\n};\n"
  },
  {
    "path": "client/src/utils/file.js",
    "content": "import siteConfig from '@config/siteConfig.json';\n\nconst {\n  publishing: { maxSizeImage = 10000000, maxSizeGif = 50000000, maxSizeVideo = 50000000 },\n} = siteConfig;\n// TODO: central constants location\nconst SIZE_MB = 1000000;\n\nexport function validateFile(file) {\n  if (!file) {\n    throw new Error('no file provided');\n  }\n  if (/'/.test(file.name)) {\n    throw new Error('apostrophes are not allowed in the file name');\n  }\n  // validate size and type\n  switch (file.type) {\n    case 'image/jpeg':\n    case 'image/jpg':\n    case 'image/png':\n    case 'image/svg+xml':\n      if (file.size > maxSizeImage) {\n        throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`);\n      }\n      break;\n    case 'image/gif':\n      if (file.size > maxSizeGif) {\n        throw new Error(`Sorry, .gifs are limited to ${maxSizeGif / SIZE_MB} megabytes.`);\n      }\n      break;\n    case 'video/mp4':\n      if (file.size > maxSizeVideo) {\n        throw new Error(`Sorry, videos are limited to ${maxSizeVideo / SIZE_MB} megabytes.`);\n      }\n      break;\n    default:\n      throw new Error(\n        file.type +\n          ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.'\n      );\n  }\n}\n"
  },
  {
    "path": "client/src/utils/oEmbed.js",
    "content": "const rel = 'alternate';\nconst title = 'spee.ch oEmbed profile';\n\nconst formatUrlForQuery = (url) => {\n  return url.replace(/\\//g, '%2F').replace(/:/g, '%3A');\n};\n\nconst createJsonLinkData = (host, canonicalUrl) => {\n  return {\n    rel,\n    type: 'application/json+oembed',\n    href: `${host}/api/oembed?url=${formatUrlForQuery(canonicalUrl)}%2F&format=json`,\n    title,\n  };\n};\n\nconst createXmlLinkData = (host, canonicalUrl) => {\n  return {\n    rel,\n    type: 'application/xml+oembed',\n    href: `${host}/api/oembed?url=${formatUrlForQuery(canonicalUrl)}%2F&format=xml`,\n    title,\n  };\n};\n\nexport default {\n  json: createJsonLinkData,\n  xml : createXmlLinkData,\n};\n"
  },
  {
    "path": "client/src/utils/publish.js",
    "content": "export const createPublishMetadata = (\n  claim,\n  { type },\n  { title, description, license, licenseUrl, nsfw },\n  publishInChannel,\n  selectedChannel\n) => {\n  // this metadata stuff needs to be removed...\n  let metadata = {\n    name: claim,\n    title,\n    description,\n    license,\n    licenseUrl,\n    nsfw,\n    type,\n  };\n  if (publishInChannel) {\n    metadata['channelName'] = selectedChannel;\n  }\n  return metadata;\n};\n\nexport const createPublishFormData = (file, thumbnail, metadata) => {\n  let fd = new FormData();\n  // append file\n  if (file) {\n    fd.append('file', file);\n  }\n  // append thumbnail\n  if (thumbnail) {\n    fd.append('thumbnail', thumbnail);\n  }\n  // append metadata\n  for (let key in metadata) {\n    if (metadata.hasOwnProperty(key)) {\n      fd.append(key, metadata[key]);\n    }\n  }\n  return fd;\n};\n\nexport const createThumbnailUrl = (channel, channelId, claim, host) => {\n  return `${host}/${channel}:${channelId}/${claim}-thumb.jpg`;\n};\n"
  },
  {
    "path": "client/src/utils/request.js",
    "content": "import 'cross-fetch/polyfill';\n\nfunction parseJSON (response) {\n  if (response.status === 204 || response.status === 205) {\n    return null;\n  }\n  return response.json();\n}\n\nfunction checkStatus (response, jsonResponse) {\n  if (response.status >= 200 && response.status < 300) {\n    return jsonResponse;\n  }\n  const error = new Error(jsonResponse.message);\n  error.response = response;\n  throw error;\n}\n\nexport default function request (url, options) {\n  return fetch(url, options)\n    .then(response => {\n      return Promise.all([response, parseJSON(response)]);\n    })\n    .then(([response, jsonResponse]) => {\n      return checkStatus(response, jsonResponse);\n    });\n}\n"
  },
  {
    "path": "client/src/utils/validate.js",
    "content": "export const validateChannelSelection = (publishInChannel, selectedChannel, loggedInChannel) => {\n  if (publishInChannel && (selectedChannel !== loggedInChannel.name)) {\n    throw new Error('Log in to a channel or select Anonymous');\n  }\n};\n\nexport const validateNoPublishErrors = ({file, url, channel}) => {\n  if (file || url || channel) {\n    throw new Error('Fix the errors identified in red');\n  }\n};\n\nexport const validateCreateChannelNameInput = ({value, error}) => {\n  if (!value) {\n    throw new Error('Please enter a channel name');\n  }\n  if (error) {\n    throw new Error(error);\n  }\n};\n\nexport const validateCreateChannelPasswordInput = ({value, error}) => {\n  if (!value) {\n    throw new Error('Please enter a password');\n  }\n  if (error) {\n    throw new Error(error);\n  }\n};\n"
  },
  {
    "path": "customize.md",
    "content": "# Configure your own spee.ch\n\n_note: this guide assumes you have done the [quickstart](https://github.com/lbryio/spee.ch/blob/master/README.md) or [fullstart](https://github.com/lbryio/spee.ch/blob/master/fullstart.md) guide and have a working spee.ch server_\n\n## Custom Components\nThe components used by spee.ch are taken from the `client/` folder, but you can override those components by defining your own in the `site/custom/` folder.\n\n### Add a new custom Logo component.\n\nTo create your own custom component to override the defaults, create a folder and an `index.jsx` file for the component in the `site/custom/src/components/` folder.\n\n```\n$ cd site/custom/src/components/\n$ mkdir Logo\n$ cd Logo\n$ touch index.jsx\n$ nano index.jsx\n```\n\nCreate a simple react component in `index.jsx`.\n\n```\nimport React from 'react';\n\nfunction Logo () {\n  return (\n    <p>My Logo</p>\n  );\n};\n\nexport default Logo;\n```\n\nRebuild and restart the server, and you should see your site with a new Logo in the top left corner!\n```\n$ npm run build\n```\nThen\n```\n$ npm run start\n```\n"
  },
  {
    "path": "devConfig/sequelizeCliConfig.example.js",
    "content": "const sequelizeCliConfig = {\n  development: {\n    username: '',\n    password: '',\n    database: '',\n    host    : '127.0.0.1',\n    dialect : 'mysql',\n  },\n  test: {\n    username: '',\n    password: '',\n    database: '',\n    host    : '127.0.0.1',\n    dialect : 'mysql',\n  },\n  production: {\n    username: '',\n    password: '',\n    database: '',\n    host    : '127.0.0.1',\n    dialect : 'mysql',\n  },\n};\n\nmodule.exports = sequelizeCliConfig;\n"
  },
  {
    "path": "devConfig/testingConfig.example.js",
    "content": "module.exports = {\n  testChannel        : null, // a channel to make test publishes in\n  testChannelId      : null, // the claim id for the test channel\n  testChannelPassword: null, // password for the test channel\n};\n"
  },
  {
    "path": "docs/settings.md",
    "content": "Settings found in cli/defaults/siteConfig.json will be copied to /site/config/siteConfig.json by running npm run configure\n\nYou are encouraged to dig into those settings to make your installation behave how you wish. Below is a description of settings available.\n\nANALYTICS:\n\n    \"googleId\": null\n\nASSET DEFAULTS: _These are some default values for publishes_\n\n    \"title\": \"Default Content Title\",\n    \"description\": \"Default Content Description\",\n    \"thumbnail\": \"https://spee.ch/0e5d4e8f4086e13f5b9ca3f9648f518e5f524402/speechflag.png\"\n\nDETAILS:\n\n    \"port\": 3000, - this is the internal server port for the application_\n    \"title\": \"My Site\",\n    \"ipAddress\": \"\",\n    \"host\": \"https://www.example.com\", - must contain \"http(s)://\" and if localhost, \"http://localhost:3000\"\n    \"description\": \"A decentralized hosting platform built on LBRY\",\n    \"twitter\": false,\n    \"blockListEndpoint\": - the LBRY default endpoint is generally for the US. Empty string \"\" negates.\n\nPUBLISHING:\n\n    \"primaryClaimAddress\": null, - generally supplied by your lbrynet sdk\n    \"uploadDirectory\": \"/home/lbry/Uploads\", - lbrynet sdk will know your uploads are here\n    \"thumbnailChannel\": null, - when publishing non-image content, thumbnails will go here.\n    \"thumbnailChannelId\": null,\n    \"additionalClaimAddresses\": [],\n    \"disabled\": false,\n    \"disabledMessage\": \"Default publishing disabled message\",\n    \"closedRegistration\": false, - true: prevent new channels from being registered\n    \"serveOnlyApproved\": false, - true: prevent your site from serving up unapproved channels\n    \"publishOnlyApproved\": false, - true: restrict\n    \"approvedChannels\": [], - If either of the above two are true, ['@MyKittens', '@BobsKittens']\n    \"publishingChannelWhitelist\": [],\n    \"channelClaimBidAmount\": \"0.1\", - When creating a channel, how much you deposit to control the name\n    \"fileClaimBidAmount\": \"0.01\", - When publishing content, how much you deposit to control the name\n    \"fileSizeLimits\": {\n      \"image\": 50000000,\n      \"video\": 50000000,\n      \"audio\": 50000000,\n      \"text\": 10000000,\n      \"model\": 50000000,\n      \"application\": 500000000,\n      \"customByContentType\": {\n        \"application/octet-stream\": 50000000\n      }\n    }\n\nSERVING:\n\n    \"dynamicFileSizing\": {\n      \"enabled\": false, - if you choose to allow your instance to serve transform images\n      \"maxDimension\": 2000 - the maximum size you allow transform to scale\n    },\n    \"markdownSettings\": {\n      \"skipHtmlMain\": true, - false: render html, in a somewhat unpredictable way~\n      \"escapeHtmlMain\": true, - true: rather than render html, escape it and print it visibly\n      \"skipHtmlDescriptions\": true, - as above, for descriptions\n      \"escapeHtmlDescriptions\": true, - as above, for descriptions\n      \"allowedTypesMain\": [], - markdown rendered as main content\n      \"allowedTypesDescriptions\": [], - markdown rendered in description in content details\n      \"allowedTypesExample\": [ - here are examples of allowed types\n        \"see react-markdown docs\", `https://github.com/rexxars/react-markdown`\n        \"root\",\n        \"text\",\n        \"break\",\n        \"paragraph\",\n        \"emphasis\",\n        \"strong\",\n        \"thematicBreak\",\n        \"blockquote\",\n        \"delete\",\n        \"link\",\n        \"image\", - you may not have a lot of control over how these are rendered\n        \"linkReference\",\n        \"imageReference\",\n        \"table\",\n        \"tableHead\",\n        \"tableBody\",\n        \"tableRow\",\n        \"tableCell\",\n        \"list\",\n        \"listItem\",\n        \"heading\",\n        \"inlineCode\",\n        \"code\",\n        \"html\", - potentially DANGEROUS, intended for `serveOnlyApproved = true` environments, includes iframes, divs.\n        \"parsedHtml\"\n      ],\n    },\n    \"customFileExtensions\": { - suggest a file extension for experimental content types you may be publishing\n      \"application/example-type\": \"example\"\n    }\n\nSTARTUP:\n\n    \"performChecks\": true,\n    \"performUpdates\": true\n\n}\n"
  },
  {
    "path": "docs/setup/conf/caddy/Caddyfile.template",
    "content": "# Replace {{EXAMPLE.COM}} with 'yourdomain.com', omitting quotes\n\nwww.{{EXAMPLE.COM}} {\n  redir https://{{EXAMPLE.COM}}\n}\n\n{{EXAMPLE.COM}} {\n  proxy / localhost:3000\n}\n"
  },
  {
    "path": "docs/setup/conf/caddy/caddy.service",
    "content": "[Unit]\nDescription=Caddy HTTP/2 web server\n\n[Service]\nUser=www-data\nGroup=www-data\nEnvironment=CADDYPATH=/opt/caddy/store\nExecStart=/usr/local/bin/caddy -agree=true -log=/opt/caddy/logs/caddy.log -conf=/opt/caddy/Caddyfile -root=/dev/null\nExecReload=/bin/kill -USR1 $MAINPID\nLimitNOFILE=1048576\nLimitNPROC=64\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "docs/setup/conf/lbrynet/lbrynet.service.example",
    "content": "[Unit]\nDescription=\"LBRYnet daemon\"\nAfter=network.target\n\n[Service]\nEnvironment=\"HOME=/home/lbry\"\nExecStart=/opt/lbry/lbrynet start\nUser=lbry\nGroup=lbry\nRestart=on-failure\nKillMode=process\n\n[Install]\nWantedBy=multi-user.target\n\n"
  },
  {
    "path": "docs/setup/conf/lbrynet/lbrynet.service.template",
    "content": "[Unit]\nDescription=\"LBRYnet daemon\"\nAfter=network.target\n\n# Change environment to /home/{{USERNAME}}\n[Service]\nEnvironment=\"HOME=/home/{{USERNAME}}\"\nExecStart=/opt/lbry/lbrynet start\nUser={{USERNAME}}\nGroup={{USERNAME}}\nRestart=on-failure\nKillMode=process\n\n[Install]\nWantedBy=multi-user.target\n\n"
  },
  {
    "path": "docs/setup/conf/nginx/letsencrypt.conf",
    "content": "#/etc/nginx/snippets/letsencrypt.conf\n\nlocation ^~ /.well-known/acme-challenge/ {\n  allow all;\n  root /var/lib/letsencrypt/;\n  default_type \"text/plain\";\n  try_files $uri =404;\n}\n"
  },
  {
    "path": "docs/setup/conf/nginx/myspeech",
    "content": "#/etc/nginx/sites-available/myspeech\n\nserver {\n  listen 80;\n  listen [::]:80;\n\n  server_name {{DOMAIN_NAME}} {{WWW_DOMAIN_NAME}}\n  include snippets/letsencrypt.conf;\n  return 301 https://$host$request_uri;\n}\n\nserver {\n  listen 443 ssl http2;\n  server_name {{WWW_DOMAIN_NAME}};\n  ssl_certificate /etc/letsencrypt/live/{{DOMAIN_NAME}}/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN_NAME}}/privkey.pem;\n  ssl_trusted_certificate /etc/letsencrypt/live/{{DOMAIN_NAME}}/chain.pem;\n  include snippets/ssl.conf;\n  include snippets/letsencrypt.conf;\n\n  access_log /var/log/nginx/www-myspeech.access.log;\n  error_log /var/log/nginx/www-myspeech.error.log;\n\n  return 301 https://{{DOMAIN_NAME}}$request_uri;\n}\n\nserver {\n  #YOUR SITE HERE\n  listen 443 ssl http2;\n  server_name {{DOMAIN_NAME}};\n\n  ssl_certificate /etc/letsencrypt/live/{{DOMAIN_NAME}}/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN_NAME}}/privkey.pem;\n  ssl_trusted_certificate /etc/letsencrypt/live/{{DOMAIN_NAME}}/chain.pem;\n  include snippets/ssl.conf;\n  include snippets/letsencrypt.conf;\n\n  access_log /var/log/nginx/myspeech.access.log;\n  error_log /var/log/nginx/myspeech.error.log;\n\n  location / {\n    proxy_read_timeout 5m;\n    proxy_pass http://localhost:3000;\n    proxy_set_header X-Real-IP  $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Forwarded-Port $server_port;\n    proxy_set_header Host $host;\n    proxy_pass_header Server;\n  }\n}\n"
  },
  {
    "path": "docs/setup/conf/nginx/ssl.conf",
    "content": "#/etc/nginx/snippets/ssl.conf\n\nssl_dhparam /etc/ssl/certs/dhparam.pem;\n\nssl_session_timeout 1d;\nssl_session_cache shared:SSL:50m;\nssl_session_tickets off;\n\nssl_protocols TLSv1 TLSv1.1 TLSv1.2;\nssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';\nssl_prefer_server_ciphers on;\n\nssl_stapling on;\nssl_stapling_verify on;\nresolver 8.8.8.8 8.8.4.4 valid=300s;\nresolver_timeout 30s;\n\nadd_header Strict-Transport-Security \"max-age=15768000; includeSubdomains; preload\";\nadd_header X-Frame-Options SAMEORIGIN;\nadd_header X-Content-Type-Options nosniff;\n"
  },
  {
    "path": "docs/setup/conf/speech/chainqueryConfig.json",
    "content": "{\n  \"host\": \"public.chainquery.lbry.io\",\n  \"port\": \"3306\",\n  \"timeout\": 30,\n  \"database\": \"chainquery\",\n  \"username\": \"speechpublic\",\n  \"password\": \"7uITJLwZRvHBZYS3JZDykD1-7hLVkVA1jDWfcgqi6QnC\"\n}\n"
  },
  {
    "path": "docs/setup/conf/speech/speech.service.draft",
    "content": "[Service]\nExecStart=/usr/bin/node /opt/app/app.js\nRestart=always\nStandardOutput=syslog\nStandardError=syslog\nSyslogIdentifier=node-app-1\nUser=your_app_user_name\nGroup=your_app_user_name\nEnvironment=NODE_ENV=production PORT=3000\n"
  },
  {
    "path": "docs/setup/scripts/firewall.sh",
    "content": "#!/bin/bash\nsudo ufw status\nsudo ufw allow 80\nsudo ufw allow 443\nsudo ufw allow 22\nsudo ufw allow 3333\nsudo ufw allow 4444\nsudo ufw default allow outgoing\nsudo ufw default deny incoming\nsudo ufw show added\nsudo ufw enable\nsudo ufw status\n"
  },
  {
    "path": "docs/setup/scripts/newuser.sh",
    "content": ""
  },
  {
    "path": "docs/ubuntuinstall.md",
    "content": "# Create Your Own Spee.ch on Ubuntu 16.x 18.x VPS\n\n# Overview\n\n## Prerequisites\n  * Ability to use SSH (putty + public key for windows users)\n  * Ubuntu 16.04 or 18.04 VPS with root access\n    * Your login info ready\n    * Exposed ports: 22, 80, 443, 3333, 4444\n  * Domain name with @ and www pointed at your VPS IP\n    * _alternatively, specify http://localhost:3000 as domain during speech configuration_\n  * Ability to send 5+ LBRY credits to an address\n  * Noncommercial use\n    * _alternative configuration examples for nginx and certbot are [here](https://github.com/lbryio/spee.ch/tree/master/docs/setup/conf/nginx)_\n\n## You'll be installing:\n  * MySQL DB version 5.7 or higher\n    * Default Port 3306\n    * mysql_native_password plugin\n  * NodeJS v8+\n  * Caddy - https reverse proxy server\n    * automatically obtains tls certificate\n    * Redirects 80 (http) to 443 (https) to Speech on 3000 \n  * Lbrynet DAEMON started on ports 3333 and 4444\n  * Spee.ch started on port 3000\n\n_note: throughout this guide you'll be replacing `{{xyz}}` with `yourvalue` omitting the brackets_\n\n# 1. Setup OS and install dependencies\n## OS\n\n### Secure your server by creating a non-root sudoer.\n\nAs root# _create user and add to sudo group_\n```\n  adduser username\n  usermod -aG sudo username\n  su - username\n```\nAs username: *paste public key in authorized\\_keys*\n\n  `cd`\n  \n  `mkdir .ssh`\n  \n  `nano ~/.ssh/authorized_keys`\n\n### Prep\n\nssh to username@domainname or username@ip_address\n\n  ```\n  sudo apt-get update -y\n  ulimit -n 8192\n  wget -qO- https://deb.nodesource.com/setup_8.x | sudo -E bash -\n  ```\n\n\n## Git, Curl, Unzip, ffmpeg, imagemagick, Node\n\n  `sudo apt-get install git curl unzip ffmpeg nodejs imagemagick -y`\n\n## Clone speech either from your own fork, or from the lbryio/spee.ch repo.\n\n  * For Developers - our master branch\n\n  `git clone https://github.com/lbryio/spee.ch`\n\n  * For Developers - your fork\n\n  `git clone https://github.com/{{youraccount}}/spee.ch.git`\n\n  `git clone git@github.com:{{youraccount}}/spee.ch` \n\n  * For Publishers and Content creators - stable release\n\n  `git clone -b release https://github.com/lbryio/spee.ch`\n\n## Prepare the scripts\n\n  `chmod 750 -R ~/spee.ch/docs/setup`\n\n# 2 Secure the UFW firewall\n\n## UFW\n\n  `sudo ~/spee.ch/docs/setup/scripts/firewall.sh`\n\n  _if your distro isn't vanilla ubuntu 16 or 18, you may have to install it_\n\n# 3 Install Caddy to handle https and reverse proxy\n\n##  Get Caddy\n\n  `curl https://getcaddy.com | sudo bash -s personal`\n\n## Set up Caddy reverse proxy and ssl\n\n  _Make Caddy's folders, copy the template, edit the Caddyfile, copy the caddyfile to its folder._\n\n  ```\n  sudo mkdir -p /opt/caddy/logs/\n  sudo mkdir -p /opt/caddy/store/\n  cp ~/spee.ch/docs/setup/conf/caddy/Caddyfile.template ~/spee.ch/docs/setup/conf/caddy/Caddyfile\n  nano ~/spee.ch/docs/setup/conf/caddy/Caddyfile\n   ```\n\n   ( Change {{EXAMPLE.COM}} to YOURDOMAIN.COM )\n\n  `sudo cp ~/spee.ch/docs/setup/conf/caddy/Caddyfile /opt/caddy/`\n\n## Set up Caddy to run as systemd service\n\n  ```\n  sudo cp ~/spee.ch/docs/setup/conf/caddy/caddy.service /etc/systemd/system/caddy.service\n  sudo chmod 644 /etc/systemd/system/caddy.service\n  sudo chown -R www-data:www-data /opt/caddy/\n  sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy\n  sudo systemctl daemon-reload\n  sudo systemctl start caddy\n  sudo systemctl status caddy\n  ```\n\n  `q` exits\n\n  At this point, navigating to yourdomain.com should give you a 502 bad gateway error. That's good!\n\n  Now you can make sure caddy starts when the machine starts:\n\n  `sudo systemctl enable caddy`\n\n\n# 4 Set up MySQL\n\n## Install MySQL\n\n  `sudo apt-get install mysql-server -y`\n\n  ( During install, enter blank password each time if prompted. We'll set one during secure setup.)\n\n  `sudo systemctl status mysql` (q to exit)\n\n## Secure Setup\n\n  `sudo mysql_secure_installation`\n  \n  * Password your_mysql_password\n  * No to password validation\n  * Y to all other options\n\n\n## Login to mysql from root to complete setup:\n\n  `sudo mysql` to enter mysql> console\n\n  mysql>\n\n  `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword123';`\n\n  mysql>\n\n  `FLUSH PRIVILEGES;`\n\n  `Control+D` to exit\n\n  Verify:\n\n  `mysql -u root -p` and then entering your_mysql_password should give you the mysql> shell\n\n# 5 Get Lbrynet SDK Daemon\n\n## Get the SDK\n\n  We'll be putting it in /opt/lbry.\n  \n  `sudo mkdir /opt/lbry`\n  \n  `sudo wget -O /opt/lbry/latest_daemon.zip https://lbry.io/get/lbrynet.linux.zip`\n  \n  `sudo unzip -o -u /opt/lbry/latest_daemon.zip -d /opt/lbry`\n\n## Set up lbrynet to run as systemd service\n\n  We'll soon update the setup scripts. Meanwhile, here's an example lbrynet.service file. Again, fully replace {{USERNAME}}\n  \n  ```\n  [Unit]\nDescription=\"LBRYnet daemon\"\nAfter=network.target\n\n# Replace {{USERNAME}} with your username, e.g. `bob` or `speechuser`\n[Service]\nEnvironment=\"HOME=/home/{{USERNAME}}\"\nExecStart=/opt/lbry/lbrynet start\nUser={{USERNAME}}\nGroup={{USERNAME}}\nRestart=on-failure\nKillMode=process\n\n[Install]\nWantedBy=multi-user.target\n\n```\n`sudo nano /etc/systemd/system/lbrynet.service`\n\n\nThen paste the above into the file and edit replacing {{USERNAME}} with yours.\n\nFinally do the following.\n\n```\n sudo chmod 644 /etc/systemd/system/lbrynet.service\n sudo systemctl daemon-reload\n sudo systemctl start lbrynet\n sudo systemctl status lbrynet\n```\n\nYou'll find your lbrynet logs in ~/.local/share/lbry/lbrynet/lbrynet.log\n\nLet's make our lives easier and link /opt/lbry/lbrynet in /usr/local/bin\n\n  `sudo ln -s /opt/lbry/lbrynet /usr/local/bin/lbrynet`\n\nNow we can `lbrynet` without `/opt/lbry`. Let's make sure we're back in our home directory.  `cd`\n\n## Customize SDK settings\n\n  These settings will prevent you and your users from spending your server's LBC on paid content. Full documentation is [here](https://lbry.tech/resources/daemon-settings).\n  \n  ~$\n  \n  `cd ~/.local/share/lbry/lbrynet`\n  \n  `nano daemon_settings.yml`\n  \n   copy and paste in the following code (Ctrl+Shift V)\n\n  _upnp is unnecessary for a vps but may be useful behind a properly configured NAT_\n\n  ```\n  run_reflector_server: false\n  max_key_fee: {amount: 0, currency: LBC}\n  use_upnp: false\n  auto_re_reflect_interval: 0\n ```\n \n `CONTROL+O` then `CONTROL+X` to save and exit\n  \n  Restart lbrynet sdk:\n  \n  `sudo systemctl restart lbrynet`\n  \n  `sudo systemctl status lbrynet`\n  \n\n## Display wallet address to which to send 5+ LBC.\n\n  _note: These commands work when `lbrynet` is already running_\n\n  `lbrynet commands` to check out the current commands\n\n  `lbrynet address list` to get your wallet address\n\n  `Ctrl + Shift + C` after highlighting an address to copy.\n\n  Use a LBRY app or daemon to send LBC to the address. Sending LBC may take a few seconds or longer.\n\n  `lbrynet account balance` to check your balance after you've sent LBC.\n\n# 6 Set up spee.ch\n\n## Build it\n\n  `cd spee.ch`\n\n  ~/spee.ch:\n\n  `npm install`\n\n  _note: if you have installed your own local chainquery instance, you will need to specify it in your own /site/config/chainqueryConfig.json_\n\n  Once your wallet has a balance, run this:\n\n  `npm run configure`    \n\n  The script will ask for the following values:\n   \n    * Database: lbry\n    * Username: root\n    * Password: your_mysql_password\n    * Port: 3000\n    * Site Title: Your Site Name\n    * Enter your site's domain name: https://example.com or http://localhost:3000\n    * Enter a directory where uploads should be stored: (/home/{{username}}/Uploads) *\n    _* if you're not sure, `pwd`_\n     \n  `npm run build`  (or `npm run dev` to build for developing)\n\n  `npm run start`\n\n## Try it\n\n  Navigate to example.com!\n\n# 7 Production\n\n## pm2 to keep your speech app running\n\nIf your server is running in the terminal from the last section, `Control+C` it.\n\n `sudo npm install -g pm2` _There are tutorials online for avoiding sudo for npm i -g_\n\n `cd spee.ch`\n\n `pm2 start npm --name speech -- run start`\n\n While pm2 installed this way will restart the server, it will not rebuild it on changes. You'll do that manually as discussed before.\n\n### 7 Maintenance Procedures\n\n#### Update sdk daemon\n\n  * Backup wallet (private keys!) to a safe place. It should be in ~/.local/share/lbry/lbryum/wallets.\n  * `lbrynet stop`\n  * Following the instructions in 5: Get the SDK will rename the old daemon and give you the new one.\n  * `lbrynet start`\n  * `lbrynet version`\n  * `lbrynet account balance`\n\n#### Update speech\n\n * Read the release notes to see if there are any breaking changes, address them\n * `pm2 stop speech`\n * `git pull origin release` (assuming you cloned release)\n * `npm i` if necessary\n * `npm run build`\n * `pm2 start speech`\n * Have an exotic energy drink\n"
  },
  {
    "path": "fullstart.md",
    "content": "# Create Your Own Spee.ch!\n\n## 1. Prerequisites\n\n### You will need the following tools installed\n\n- Node (v8 LTS).\n- Make sure you install from the **Node** website [link](https://nodejs.org/en/download/).\n- npm (should come installed with Node).\n- Git\n- Curl\n- Tmux\n- Unzip\n\n### Make sure **npm** is up-to-date.\n\n```\n$ npm update\n```\n\n### Setup a Webserver to serve **Spee.ch** from Port **3000**.\n\n- If you are using a server provided by **lbry**, we will have **caddy** installed already.\n- If you are using your own server, make sure to have a web server installed and set up to serve from port **3000**.\n- Nginx instructions (recommended).\n\n  - Insert directions for certbot before installing.\n  - Install [Nginx](http://nginx.org/en/docs/install.html).\n  - Create a config file called `spee.ch` in _/etc/nginx/sites-available_\n  - see example: [config file](https://github.com/lbryio/spee.ch/blob/master/nginx_example_config).\n  - Rename all mentions of _sub.domain.com_ with your subdomain name.\n  - Run this command to link the sites-available.\n\n    `$ ln -s /etc/nginx/sites-available/speech /etc/nginx/sites-enabled/speech`\n\n  - Restart Nginx.\n\n    `$ sudo service nginx restart`\n\n  - Try visiting your website.\n    - If Nginx is working, you should get a **502** error because there is nothing running on **3000** yet.\n    - If you get the default Nginx greeting, you have not properly configured it to serve from port **3000**.\n    - You can find logs in _/var/log/nginx/_ too.\n  - Caddy tutorial: [https://caddyserver.com/tutorial](https://caddyserver.com/tutorial)\n\n### MySql\n\n- Install MySql\n  - [Instructions](https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en)\n- Create user **root**. \\* Note: We are going to access **mysql** as **root** for this setup, but you may want to create a separate user in the future.\n  - Keep your password somewhere handy!\n- Create a database called **lbry** and make sure you can use it.\n\n      \t  `CREATE DATABASE lbry;`\n\n      \t  `$ USE lbry;`\n\n      \t  `$ exit; (or press ‘ctl + d’)`\n\n- Try logging into mysql.\n\n      \t  `$ mysql -u username -p`\n\n- If you are using a **LBRY** server, your **password** is the one provided for **ssh**.\n  - Note: If it fails, try using `sudo`.\n\n##2. Install & Run the LBRY Daemon\n\n### Install **lbrynet**\n\n_note: if you have a server from LBRY, lbrynet is already installed, you can skip to 2.4._\n\n```\n$ wget --quiet -O ~/latest_daemon.zip https://lbry.com/get/lbrynet.linux.zip\n$ unzip -o -u \"~/latest_daemon.zip\"\n```\n\n### Start lbrynet\n\n```\n$ tmux\n$ ./lbrynet-daemon\n```\n\n### Detach (exit) the tmux session and leave **lbrynet** running in the background.\n\npress `ctrl` + `b` then `d` to detach\n\n### Get LBC!\n\nGet a list of your wallets:\n\n```\n$ ~/lbrynet-cli wallet_list\n```\n\nSend some LBC to one of the addresses from your wallet.\n\nCheck your balance again:\n\n```\n$ ~/lbrynet-cli wallet_balance\n```\n\nYou should have **LBC**!\n\n### Install ffmpeg\n\ndirections: [here](https://www.ffmpeg.org/download.html)\n\n## 3. Set up Spee.ch\n\n### Clone the spee.ch repo\n\n```\n$ git clone https://github.com/lbryio/www.spee.ch.git\n```\n\nChange directory into your site’s folder\n\n```\n$ cd <name-of-your-site>` or `$ cd www.spee.ch\n```\n\nInstall dependencies\n\n```\n$ npm install\n```\n\nRun the config cli:\n\n```\n$ npm run configure\n```\n\nCheck your site configs\n\n```\n$ cd /site/config/\n$ nano siteConfig.json\n```\n\n### Build & run\n\nRun the below command to transpile, build, and start your server.\n\n```\n$ npm run start\n```\n\n_**Note:** if you had to use `sudo` to login to **mysql** above, you may have issues with this step._\n\nSpee.ch should now be running !\n\nVisit your site in the browser. Try publishing an image!\n\n## 4. Bonus:\n\n### Install PM2 and run your server with PM2\n\nInstall PM2\n\n```\n$ sudo npm i -g pm2\n```\n\nFrom inside your project’s folder, start your server with PM2.\n\n```\n$ pm2 start server.js\n```\n\nVisit your site and see if it is running!\n"
  },
  {
    "path": "lintstagedrc.json",
    "content": "{\n  \"linters\": {\n    \"src/**/*.{js,jsx,scss,json}\": [\"prettier --write\", \"git add\"],\n    \"src/**/*.{js,jsx}\": [\"eslint --fix\", \"flow focus-check --color always\", \"git add\"]\n  }\n}\n"
  },
  {
    "path": "nginx_example_config",
    "content": "# Speech nginx configuration\n\nserver {\n  listen 80;\n  server_name  sub.domain.com;\n\n  listen 443 ssl;\n\n  ssl_certificate     /etc/letsencrypt/live/sub.domain.com/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/sub.domain.com/privkey.pem;\n\n  ssl_session_cache shared:SSL:20m;\n  ssl_session_timeout 1440m;\n  ssl_protocols TLSv1.1 TLSv1.2;\n  ssl_prefer_server_ciphers on;\n  # Using list of ciphers from https://github.com/cloudflare/sslconfig\n  ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;\n\n  # if letsencrypt is not working, this is probably preventing it from getting to .well-known\n  if ($scheme = http) {\n    rewrite ^ https://$server_name$request_uri? permanent;\n  }\n\n#  gzip_static on;\n#  gzip_http_version 1.0;\n\n  access_log /var/log/nginx/speech_nginx_access_log;\n  error_log /var/log/nginx/speech_nginx_error_log;\n\n  location / {\n    proxy_read_timeout 5m;\n    proxy_pass http://127.0.0.1:3000;\n    proxy_set_header X-Real-IP  $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Forwarded-Port $server_port;\n    proxy_set_header Host $host;\n    proxy_pass_header Server;\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"spee.ch\",\n  \"version\": \"0.1.1\",\n  \"description\": \"an npm package that exports a customizeable spee.ch server\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"webpack --config webpack.config.js --mode=production\",\n    \"dev\": \"webpack --config webpack.config.js --mode=development\",\n    \"configure\": \"node cli/configure.js\",\n    \"fix\": \"eslint . --fix\",\n    \"lint\": \"eslint .\",\n    \"start\": \"node server/bundle/server.js\",\n    \"devtools:server\": \"ndb server.js\",\n    \"devtools:chainquery\": \"npm run devtools:chainquery:build && ndb ./server/chainquery/bundle.debug.js\",\n    \"devtools:chainquery:build\": \"rollup ./server/chainquery/index.debug.js --file ./server/chainquery/bundle.debug.js --format cjs\",\n    \"test\": \"mocha --recursive\",\n    \"test:no-lbc\": \"npm test -- --grep @usesLbc --invert\",\n    \"test:server\": \"mocha --recursive './server/**/*.test.js'\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lbryio/spee.ch.git\"\n  },\n  \"keywords\": [\n    \"spee.ch\",\n    \"lbry\",\n    \"blockchain\"\n  ],\n  \"author\": \"@lbryio\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/lbryio/spee.ch/issues\"\n  },\n  \"homepage\": \"https://github.com/lbryio/spee.ch#readme\",\n  \"dependencies\": {\n    \"@fortawesome/fontawesome-svg-core\": \"^1.2.8\",\n    \"@fortawesome/free-solid-svg-icons\": \"^5.5.0\",\n    \"@fortawesome/react-fontawesome\": \"^0.1.3\",\n    \"axios\": \"^0.18.1\",\n    \"bcrypt\": \"^3.0.3\",\n    \"body-parser\": \"^1.18.3\",\n    \"connect-multiparty\": \"^2.2.0\",\n    \"cookie-session\": \"^2.0.0-beta.3\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.16.4\",\n    \"express-handlebars\": \"^3.0.0\",\n    \"express-http-context\": \"^1.2.0\",\n    \"generate-password\": \"^1.4.1\",\n    \"get-video-dimensions\": \"^1.0.0\",\n    \"gm\": \"^1.23.1\",\n    \"helmet\": \"^3.15.0\",\n    \"image-size\": \"^0.6.3\",\n    \"inquirer\": \"^5.2.0\",\n    \"ip\": \"^1.1.5\",\n    \"isbot\": \"^2.2.1\",\n    \"lodash\": \"^4.17.13\",\n    \"make-dir\": \"^1.3.0\",\n    \"mime-types\": \"^2.1.21\",\n    \"module-alias\": \"^2.1.0\",\n    \"mysql2\": \"^1.6.4\",\n    \"npm\": \"^6.3.0\",\n    \"passport\": \"^0.4.0\",\n    \"passport-local\": \"^1.0.0\",\n    \"prop-types\": \"^15.6.2\",\n    \"react\": \"^16.4.2\",\n    \"react-dom\": \"^16.6.1\",\n    \"react-draggable\": \"^3.0.5\",\n    \"react-feather\": \"^1.1.4\",\n    \"react-ga\": \"^2.5.3\",\n    \"react-helmet\": \"^5.2.0\",\n    \"react-image\": \"^2.0.0\",\n    \"react-markdown\": \"^4.0.6\",\n    \"react-redux\": \"^5.1.1\",\n    \"react-router-dom\": \"^4.3.1\",\n    \"react-select\": \"^2.1.1\",\n    \"redux\": \"^4.0.1\",\n    \"redux-saga\": \"^0.16.2\",\n    \"sequelize\": \"^4.41.1\",\n    \"sequelize-cli\": \"^4.0.0\",\n    \"universal-analytics\": \"^0.4.20\",\n    \"webpack-merge\": \"^4.1.4\",\n    \"whatwg-fetch\": \"^2.0.4\",\n    \"winston\": \"^2.3.1\",\n    \"winston-slack-webhook\": \"github:billbitt/winston-slack-webhook\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.1.5\",\n    \"@babel/core\": \"^7.2.0\",\n    \"@babel/plugin-proposal-object-rest-spread\": \"^7.0.0\",\n    \"@babel/polyfill\": \"^7.0.0\",\n    \"@babel/preset-env\": \"^7.2.0\",\n    \"@babel/preset-react\": \"^7.0.0\",\n    \"@babel/preset-stage-2\": \"^7.0.0\",\n    \"@babel/register\": \"^7.0.0\",\n    \"babel-eslint\": \"9.0.0-beta.3\",\n    \"babel-loader\": \"^8.0.4\",\n    \"babel-plugin-module-resolver\": \"^3.1.1\",\n    \"chai\": \"^4.2.0\",\n    \"chai-http\": \"^4.2.0\",\n    \"cross-fetch\": \"^2.2.3\",\n    \"css-loader\": \"^2.0.0\",\n    \"eslint\": \"5.9.0\",\n    \"eslint-config-standard\": \"^12.0.0\",\n    \"eslint-config-standard-jsx\": \"^6.0.2\",\n    \"eslint-plugin-import\": \"^2.14.0\",\n    \"eslint-plugin-node\": \"^8.0.0\",\n    \"eslint-plugin-promise\": \"^4.0.1\",\n    \"eslint-plugin-react\": \"^7.11.1\",\n    \"eslint-plugin-standard\": \"^4.0.0\",\n    \"extract-css-chunks-webpack-plugin\": \"^3.2.1\",\n    \"file-loader\": \"^2.0.0\",\n    \"har-validator\": \"^5.1.3\",\n    \"husky\": \"^1.3.1\",\n    \"lint-staged\": \"^8.1.0\",\n    \"md5-file\": \"^4.0.0\",\n    \"mini-css-extract-plugin\": \"^0.5.0\",\n    \"mocha\": \"^5.2.0\",\n    \"ndb\": \"^1.0.42\",\n    \"node-sass\": \"^4.11.0\",\n    \"nodemon\": \"^1.18.6\",\n    \"prettier\": \"1.15.3\",\n    \"react-color\": \"^2.14.1\",\n    \"react-hot-loader\": \"^4.6.0\",\n    \"redux-devtools\": \"^3.4.1\",\n    \"regenerator-transform\": \"^0.13.3\",\n    \"rollup\": \"^0.67.0\",\n    \"sass-loader\": \"^7.1.0\",\n    \"sequelize-cli\": \"^4.0.0\",\n    \"style-loader\": \"^0.23.1\",\n    \"url-loader\": \"^1.1.2\",\n    \"wait-on\": \"^3.2.0\",\n    \"webpack\": \"^4.27.1\",\n    \"webpack-cli\": \"^3.1.2\",\n    \"webpack-dev-middleware\": \"^3.4.0\",\n    \"webpack-hot-middleware\": \"^2.24.3\",\n    \"webpack-node-externals\": \"^1.7.2\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"*.js\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"git add\"\n    ],\n    \"*.{json,css,md}\": [\n      \"prettier --write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "public/bundle/.gitkeep",
    "content": ""
  },
  {
    "path": "public/robots.txt",
    "content": ""
  },
  {
    "path": "server/chainquery/index.debug.js",
    "content": "console.log('Loading `chainquery`, please wait...')\n\nimport chainquery from './index'\n\nglobal.chainquery = chainquery.default ? chainquery.default : chainquery;\n\nconsole.log('`chainquery` has been loaded into the global context.')\n"
  },
  {
    "path": "server/chainquery/index.js",
    "content": "const Sequelize = require('sequelize');\nconst logger = require('winston');\n\nimport abnormalClaimTable from './tables/abnormalClaimTable';\nimport addressTable from './tables/addressTable';\nimport blockTable from './tables/blockTable';\nimport claimTable from './tables/claimTable';\nimport inputTable from './tables/inputTable';\nimport outputTable from './tables/outputTable';\nimport supportTable from './tables/supportTable';\nimport transactionAddressTable from './tables/transactionAddressTable';\nimport transactionTable from './tables/transactionTable';\n\nimport abnormalClaimQueries from './queries/abnormalClaimQueries';\nimport addressQueries from './queries/addressQueries';\nimport blockQueries from './queries/blockQueries';\nimport claimQueries from './queries/claimQueries';\nimport inputQueries from './queries/inputQueries';\nimport outputQueries from './queries/outputQueries';\nimport supportQueries from './queries/supportQueries';\nimport transactionAddressQueries from './queries/transactionAddressQueries';\nimport transactionQueries from './queries/transactionQueries';\n\nconst DATABASE_STRUCTURE = {\n  'abnormal_claim': {\n    table: abnormalClaimTable,\n    queries: abnormalClaimQueries,\n  },\n  'address': {\n    table: addressTable,\n    queries: addressQueries,\n  },\n  'block': {\n    table: blockTable,\n    queries: blockQueries,\n  },\n  'claim': {\n    table: claimTable,\n    queries: claimQueries,\n  },\n  'input': {\n    table: inputTable,\n    queries: inputQueries,\n  },\n  'output': {\n    table: outputTable,\n    queries: outputQueries,\n  },\n  'support': {\n    table: supportTable,\n    queries: supportQueries,\n  },\n  'transaction_address': {\n    table: transactionAddressTable,\n    queries: transactionAddressQueries,\n  },\n  'transaction': {\n    table: transactionTable,\n    queries: transactionQueries,\n  },\n};\n\nconst {\n  host,\n  port,\n  database,\n  username,\n  password,\n} = require('@config/chainqueryConfig');\n\nif (!database || !username || !password) {\n  logger.warn('missing database, user, or password from chainqueryConfig');\n}\n\n// set sequelize options\nconst sequelize = new Sequelize(database, username, password, {\n  host          : host,\n  import        : port,\n  dialect       : 'mysql',\n  dialectOptions: {\n    decimalNumbers: true,\n  },\n  logging: false,\n  pool   : {\n    max    : 5,\n    min    : 0,\n    idle   : 10000,\n    acquire: 10000,\n  },\n  operatorsAliases: false,\n});\n\nconst db = {};\nconst DATABASE_STRUCTURE_KEYS = Object.keys(DATABASE_STRUCTURE);\n\nfor(let i = 0; i < DATABASE_STRUCTURE_KEYS.length; i++) {\n  let dbKey = DATABASE_STRUCTURE_KEYS[i];\n  let currentData = DATABASE_STRUCTURE[dbKey];\n\n  db[dbKey] = currentData.table.createModel(sequelize, Sequelize);\n  db[dbKey].queries = currentData.queries(db, db[dbKey], sequelize);\n}\n\n// run model.association for each model in the db object that has an association\nlogger.info('associating chainquery db models...');\nDATABASE_STRUCTURE_KEYS.forEach(modelName => {\n  if (db[modelName].associate) {\n    logger.info('Associating chainquery model:', modelName);\n    db[modelName].associate(db);\n  }\n});\n\n// establish mysql connection\nsequelize\n  .authenticate()\n  .then(() => {\n    logger.info('Sequelize has established mysql connection for chainquery successfully.');\n  })\n  .catch(err => {\n    logger.error('Sequelize was unable to connect to the chainquery database:', err);\n  });\n\nexport default db;\n"
  },
  {
    "path": "server/chainquery/models/AbnormalClaimModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'abnormal_claim',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    name: {\n      type: STRING,\n      set() { },\n    },\n    claim_id: {\n      type: STRING,\n      set() { },\n    },\n    is_update: {\n      type: BOOLEAN,\n      set() { },\n    },\n    block_hash: {\n      type: STRING,\n      set() { },\n    },\n    transaction_hash: {\n      type: STRING,\n      set() { },\n    },\n    vout: {\n      type: INTEGER,\n      set() { },\n    },\n    output_id: {\n      type: INTEGER,\n      set() { },\n    },\n    value_as_hex: {\n      type: TEXT,\n      set() { },\n    },\n    value_as_json: {\n      type: TEXT,\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/AddressModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'address',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    address: {\n      type: STRING,\n      set() { },\n    },\n    first_seen: {\n      type: DATE(6),\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/BlockModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'block',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    bits: {\n      type: STRING,\n      set() { },\n    },\n    chainwork: {\n      type: STRING,\n      set() { },\n    },\n    confirmations: {\n      type: STRING,\n      set() { },\n    },\n    difficulty: {\n      type: STRING,\n      set() { },\n    },\n    hash: {\n      type: STRING,\n      set() { },\n    },\n    height: {\n      type: STRING,\n      set() { },\n    },\n    merkle_root: {\n      type: STRING,\n      set() { },\n    },\n    name_claim_root: {\n      type: STRING,\n      set() { },\n    },\n    nonce: {\n      type: STRING,\n      set() { },\n    },\n    previous_block_hash: {\n      type: STRING,\n      set() { },\n    },\n    next_block_hash: {\n      type: STRING,\n      set() { },\n    },\n    block_size: {\n      type: STRING,\n      set() { },\n    },\n    block_time: {\n      type: STRING,\n      set() { },\n    },\n    version: {\n      type: STRING,\n      set() { },\n    },\n    version_hex: {\n      type: STRING,\n      set() { },\n    },\n    transaction_hashes: {\n      type: STRING,\n      set() { },\n    },\n    transactions_processed: {\n      type: STRING,\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/ClaimModel.js",
    "content": "const logger = require('winston');\nconst mime = require('mime-types');\nconst {\n  serving: { customFileExtensions },\n} = require('@config/siteConfig');\n\nconst getterMethods = {\n  generated_extension() {\n    logger.debug('trying to generate extension', this.content_type);\n    if (customFileExtensions.hasOwnProperty(this.content_type)) {\n      return customFileExtensions[this.content_type];\n    } else {\n      return mime.extension(this.content_type) ? mime.extension(this.content_type) : 'jpg';\n    }\n  },\n};\n\nexport default (sequelize, { BOOLEAN, DATE, DECIMAL, ENUM, INTEGER, STRING, TEXT }) =>\n  sequelize.define(\n    'claim',\n    {\n      id: {\n        primaryKey: true,\n        type: INTEGER,\n        set() {},\n      },\n      transaction_hash_id: {\n        type: STRING,\n        set() {},\n      },\n      vout: {\n        type: INTEGER,\n        set() {},\n      },\n      name: {\n        type: STRING,\n        set() {},\n      },\n      claim_id: {\n        type: STRING,\n        set() {},\n      },\n      claim_type: {\n        type: INTEGER,\n        set() {},\n      },\n      publisher_id: {\n        type: STRING,\n        set() {},\n      },\n      publisher_sig: {\n        type: STRING,\n        set() {},\n      },\n      certificate: {\n        type: STRING,\n        set() {},\n      },\n      sd_hash: {\n        type: STRING,\n        set() {},\n      },\n      transaction_time: {\n        type: INTEGER,\n        set() {},\n      },\n      version: {\n        type: STRING,\n        set() {},\n      },\n      valid_at_height: {\n        type: INTEGER,\n        set() {},\n      },\n      height: {\n        type: INTEGER,\n        set() {},\n      },\n      effective_amount: {\n        type: INTEGER,\n        set() {},\n      },\n      author: {\n        type: STRING,\n        set() {},\n      },\n      description: {\n        type: STRING,\n        set() {},\n      },\n      content_type: {\n        type: STRING,\n        set() {},\n      },\n      is_nsfw: {\n        type: BOOLEAN,\n        set() {},\n      },\n      language: {\n        type: STRING,\n        set() {},\n      },\n      thumbnail_url: {\n        type: STRING,\n        set() {},\n      },\n      title: {\n        type: STRING,\n        set() {},\n      },\n      fee: {\n        type: DECIMAL(58, 8),\n        set() {},\n      },\n      fee_currency: {\n        type: STRING,\n        set() {},\n      },\n      bid_state: {\n        type: ENUM('Active', 'Expired', 'Controlling', 'Spent', 'Accepted'),\n        set() {},\n      },\n      created_at: {\n        type: DATE(6),\n        set() {},\n      },\n      modified_at: {\n        type: DATE(6),\n        set() {},\n      },\n      fee_address: {\n        type: STRING,\n        set() {},\n      },\n      claim_address: {\n        type: STRING,\n        set() {},\n      },\n      license: {\n        type: STRING,\n        set() {},\n      },\n      license_url: {\n        type: STRING,\n        set() {},\n      },\n    },\n    {\n      freezeTableName: true,\n      getterMethods,\n      timestamps: false, // don't use default timestamps columns\n    }\n  );\n"
  },
  {
    "path": "server/chainquery/models/InputModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'input',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    transaction_id: {\n      type: INTEGER,\n      set() { },\n    },\n    transaction_hash: {\n      type: STRING,\n      set() { },\n    },\n    input_address_id: {\n      type: INTEGER,\n      set() { },\n    },\n    is_coinbase: {\n      type: BOOLEAN,\n      set() { },\n    },\n    coinbase: {\n      type: STRING,\n      set() { },\n    },\n    prevout_hash: {\n      type: STRING,\n      set() { },\n    },\n    prevout_n: {\n      type: INTEGER.UNSIGNED,\n      set() { },\n    },\n    prevout_spend_updated: {\n      type: INTEGER,\n      set() { },\n    },\n    sequence: {\n      type: INTEGER,\n      set() { },\n    },\n    value: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n    script_sig_asm: {\n      type: TEXT,\n      set() { },\n    },\n    script_sig_hex: {\n      type: TEXT,\n      set() { },\n    },\n    created: {\n      type: DATE(6),\n      set() { },\n    },\n    modified: {\n      type: DATE(6),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/OutputModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'output',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    transaction_id: {\n      type: INTEGER,\n      set() { },\n    },\n    transaction_hash: {\n      type: STRING,\n      set() { },\n    },\n    value: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n    vout: {\n      type: INTEGER,\n      set() { },\n    },\n    type: {\n      type: STRING,\n      set() { },\n    },\n    script_pub_key_asm: {\n      type: TEXT,\n      set() { },\n    },\n    script_pub_key_hex: {\n      type: TEXT,\n      set() { },\n    },\n    required_signatures: {\n      type: INTEGER,\n      set() { },\n    },\n    address_list: {\n      type: TEXT,\n      set() { },\n    },\n    is_spent: {\n      type: BOOLEAN,\n      set() { },\n    },\n    spent_by_input_id: {\n      type: INTEGER,\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n    claim_id: {\n      type: STRING,\n      set() { },\n    }\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/SupportModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'support',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    supported_claim_id: {\n      type: STRING,\n      set() { },\n    },\n    support_amount: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n    bid_state: {\n      type: STRING,\n      set() { },\n    },\n    transaction_hash_id: {\n      type: STRING,\n      set() { },\n    },\n    vout: {\n      type: INTEGER,\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/TransactionAddressModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'transaction_address',\n  {\n    transaction_id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    address_id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    debit_amount: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n    credit_amount: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/models/TransactionModel.js",
    "content": "const getterMethods = {\n  // Add as needed, prefix all methods with `generated`\n}\n\nexport default (sequelize, {\n  BOOLEAN,\n  DATE,\n  DECIMAL,\n  INTEGER,\n  STRING,\n  TEXT,\n}) => sequelize.define(\n  'transaction',\n  {\n    id: {\n      primaryKey: true,\n      type: INTEGER,\n      set() { },\n    },\n    block_hash_id: {\n      type: STRING,\n      set() { },\n    },\n    input_count: {\n      type: INTEGER,\n      set() { },\n    },\n    output_count: {\n      type: INTEGER,\n      set() { },\n    },\n    fee: {\n      type: DECIMAL(18, 8),\n      set() { },\n    },\n    transaction_time: {\n      type: INTEGER,\n      set() { },\n    },\n    transaction_size: {\n      type: INTEGER,\n      set() { },\n    },\n    hash: {\n      type: STRING,\n      set() { },\n    },\n    version: {\n      type: INTEGER,\n      set() { },\n    },\n    lock_time: {\n      type: DATE(6),\n      set() { },\n    },\n    raw: {\n      type: TEXT,\n      set() { },\n    },\n    created_at: {\n      type: DATE(6),\n      set() { },\n    },\n    modified_at: {\n      type: DATE(6),\n      set() { },\n    },\n    created_time: {\n      type: DATE(6),\n      set() {},\n    },\n  },\n  {\n    freezeTableName: true,\n    //getterMethods,\n    timestamps: false, // don't use default timestamps columns\n  }\n);\n"
  },
  {
    "path": "server/chainquery/queries/abnormalClaimQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/addressQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/blockQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/claimQueries.js",
    "content": "const logger = require('winston');\n\nconst returnShortId = (claimsArray, longId) => {\n  let claimIndex;\n  let shortId = longId.substring(0, 1); // default short id is the first letter\n  let shortIdLength = 0;\n  // find the index of this claim id\n  claimIndex = claimsArray.findIndex(element => {\n    return element.claim_id === longId;\n  });\n  if (claimIndex < 0) {\n    throw new Error('claim id not found in claims list');\n  }\n  // get an array of all claims with lower height\n  let possibleMatches = claimsArray.slice(0, claimIndex);\n  // remove certificates with the same prefixes until none are left.\n  while (possibleMatches.length > 0) {\n    shortIdLength += 1;\n    shortId = longId.substring(0, shortIdLength);\n    possibleMatches = possibleMatches.filter(element => {\n      return element.claim_id && element.claim_id.substring(0, shortIdLength) === shortId;\n    });\n  }\n  return shortId;\n};\n\nconst isLongClaimId = claimId => {\n  return claimId && claimId.length === 40;\n};\n\nconst isShortClaimId = claimId => {\n  return claimId && claimId.length < 40;\n};\n\nexport default (db, table, sequelize) => ({\n  getClaimChannelName: async publisher_id => {\n    return await table\n      .findAll({\n        where: { claim_id: publisher_id },\n        attributes: ['name'],\n      })\n      .then(result => {\n        if (result.length === 0) {\n          throw new Error(`no record found for ${claimId}`);\n        } else if (result.length !== 1) {\n          logger.warn(`more than one record matches ${claimId} in db.Claim`);\n        }\n\n        return result[0].name;\n      });\n  },\n\n  getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {\n    logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);\n    return await table\n      .findAll({\n        where: { name: claimName },\n        order: [['height', 'ASC']],\n      })\n      .then(result => {\n        if (result.length === 0) {\n          throw new Error('No claim(s) found with that claim name');\n        }\n\n        let list = result.map(claim => claim.dataValues);\n        if (pendingClaim) {\n          list = list.concat(pendingClaim);\n        }\n\n        return returnShortId(list, claimId);\n      });\n  },\n\n  getAllChannelClaims: async (channelClaimId, bidState) => {\n    logger.debug(`claim.getAllChannelClaims for ${channelClaimId}`);\n    const whereClause = bidState || {\n      [sequelize.Op.or]: [\n        { bid_state: 'Controlling' },\n        { bid_state: 'Active' },\n        { bid_state: 'Accepted' },\n      ],\n    };\n    const selectWhere = {\n      ...whereClause,\n      claim_type: 1,\n      publisher_id: channelClaimId,\n    };\n    return await table\n      .findAll({\n        where: selectWhere,\n        order: [['height', 'DESC'], ['claim_id', 'ASC']],\n      })\n      .then(channelClaimsArray => {\n        if (channelClaimsArray.length === 0) {\n          return null;\n        }\n        return channelClaimsArray;\n      });\n  },\n\n  getClaimIdByLongChannelId: async (channelClaimId, claimName) => {\n    logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`);\n    return await table\n      .findAll({\n        where: {\n          name: claimName,\n          claim_type: 1,\n          publisher_id: channelClaimId,\n          bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] },\n        },\n        order: [['id', 'ASC']],\n      })\n      .then(result => {\n        switch (result.length) {\n          case 0:\n            return null;\n          case 1:\n            return result[0].claim_id;\n          default:\n            // Does this actually happen??? (from converted code)\n            logger.warn(\n              `${result.length} records found for \"${claimName}\" in channel \"${channelClaimId}\"`\n            );\n            return result[0].claim_id;\n        }\n      });\n  },\n\n  validateLongClaimId: async (name, claimId) => {\n    return await table\n      .findOne({\n        where: {\n          name,\n          claim_id: claimId,\n        },\n      })\n      .then(result => {\n        if (!result) {\n          return false;\n        }\n        return claimId;\n      });\n  },\n\n  getLongClaimIdFromShortClaimId: async (name, shortId) => {\n    return await table\n      .findAll({\n        where: {\n          name,\n          claim_id: {\n            [sequelize.Op.like]: `${shortId}%`,\n          },\n        },\n        order: [['height', 'ASC']],\n      })\n      .then(result => {\n        if (result.length === 0) {\n          return null;\n        }\n\n        return result[0].claim_id;\n      });\n  },\n\n  getTopFreeClaimIdByClaimName: async name => {\n    return await table\n      .findAll({\n        // TODO: Limit 1\n        where: { name, bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] } },\n        order: [['effective_amount', 'DESC'], ['height', 'ASC']],\n      })\n      .then(result => {\n        if (result.length === 0) {\n          return null;\n        }\n        return result[0].claim_id;\n      });\n  },\n\n  getLongClaimId: async (claimName, claimId) => {\n    // TODO: Add failure case\n    logger.debug(`getLongClaimId(${claimName}, ${claimId})`);\n    if (isLongClaimId(claimId)) {\n      return table.queries.validateLongClaimId(claimName, claimId);\n    } else if (isShortClaimId(claimId)) {\n      return table.queries.getLongClaimIdFromShortClaimId(claimName, claimId);\n    } else {\n      return table.queries.getTopFreeClaimIdByClaimName(claimName);\n    }\n  },\n\n  resolveClaim: async (name, claimId) => {\n    logger.debug(`Claim.resolveClaim: ${name} ${claimId}`);\n    return table\n      .findAll({\n        where: { name, claim_id: claimId },\n      })\n      .then(claimArray => {\n        if (claimArray.length === 0) {\n          return null;\n        } else if (claimArray.length !== 1) {\n          logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);\n        }\n\n        return claimArray[0];\n      })\n      .catch(error => {\n        logger.verbose(`resolveClaim failed: ${error}`)\n        reject(error);\n      });\n  },\n\n  resolveClaimInChannel: async (claimName, channelId) => {\n    logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);\n    return table\n      .findAll({\n        where: {\n          name: claimName,\n          claim_type: 1,\n          publisher_id: channelId,\n        },\n      })\n      .then(claimArray => {\n        if (claimArray.length === 0) {\n          return null;\n        } else if (claimArray.length !== 1) {\n          logger.warn(`more than one record matches ${claimName} in ${channelId}`);\n        }\n\n        return claimArray[0];\n      });\n  },\n\n  getOutpoint: async (name, claimId) => {\n    logger.debug(`finding outpoint for ${name}#${claimId}`);\n\n    return await table\n      .findAll({\n        where: { name, claim_id: claimId },\n        attributes: ['transaction_hash_id'],\n      })\n      .then(result => {\n        if (result.length === 0) {\n          throw new Error(`no record found for ${name}#${claimId}`);\n        } else if (result.length !== 1) {\n          logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);\n        }\n\n        return result[0].transaction_hash_id;\n      });\n  },\n\n  getCurrentHeight: async () => {\n    return await table.max('height').then(result => {\n      return result || 100000;\n    });\n  },\n});\n"
  },
  {
    "path": "server/chainquery/queries/inputQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/outputQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/supportQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/transactionAddressQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/queries/transactionQueries.js",
    "content": "export default (db, table) => ({\n  example: () => table.findAll(),\n})\n"
  },
  {
    "path": "server/chainquery/tables/abnormalClaimTable.js",
    "content": "import AbnormalClaimModel from '../models/AbnormalClaimModel';\n\nexport default {\n  createModel(...args) {\n    return AbnormalClaimModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/addressTable.js",
    "content": "import AddressModel from '../models/AddressModel';\n\nexport default {\n  createModel(...args) {\n    return AddressModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/blockTable.js",
    "content": "import BlockModel from '../models/BlockModel';\n\nexport default {\n  createModel(...args) {\n    return BlockModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/claimTable.js",
    "content": "import ClaimModel from '../models/ClaimModel';\n\nexport default {\n  createModel(...args) {\n    return ClaimModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/inputTable.js",
    "content": "import InputModel from '../models/InputModel';\n\nexport default {\n  createModel(...args) {\n    return InputModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/outputTable.js",
    "content": "import OutputModel from '../models/OutputModel';\n\nexport default {\n  createModel(...args) {\n    return OutputModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/supportTable.js",
    "content": "import SupportModel from '../models/SupportModel';\n\nexport default {\n  createModel(...args) {\n    return SupportModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/transactionAddressTable.js",
    "content": "import TransactionAddressModel from '../models/TransactionAddressModel';\n\nexport default {\n  createModel(...args) {\n    return TransactionAddressModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/chainquery/tables/transactionTable.js",
    "content": "import TransactionModel from '../models/TransactionModel';\n\nexport default {\n  createModel(...args) {\n    return TransactionModel(...args);\n  },\n\n  associate(db) {\n    // associate\n  },\n}\n"
  },
  {
    "path": "server/controllers/api/blocked/index.js",
    "content": "const logger = require('winston');\nconst db = require('../../../models');\n\nconst updateBlockedList = (req, res) => {\n  db.Blocked.refreshTable()\n    .then(data => {\n      logger.info('finished updating blocked content list');\n      res.status(200).json({\n        success: true,\n        data,\n      });\n    })\n    .catch(error => {\n      logger.error(error);\n      res.status(500).json({\n        success: false,\n        error,\n      });\n    });\n};\n\nmodule.exports = updateBlockedList;\n"
  },
  {
    "path": "server/controllers/api/channel/availability/checkChannelAvailability.js",
    "content": "const db = require('../../../../models');\n\nconst checkChannelAvailability = (name) => {\n  return db.Channel\n    .findAll({\n      where: {\n        channelName: name,\n      },\n    })\n    .then(result => {\n      return (result.length <= 0);\n    })\n    .catch(error => {\n      throw error;\n    });\n};\n\nmodule.exports = checkChannelAvailability;\n"
  },
  {
    "path": "server/controllers/api/channel/availability/index.js",
    "content": "const checkChannelAvailability = require('./checkChannelAvailability.js');\nconst { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\n/*\n\n  route to check whether site has published to a channel\n\n*/\n\nfunction addAtSymbolIfNecessary (name) {\n  if (name.substring(0, 1) !== '@') {\n    return `@${name}`;\n  }\n  return name;\n}\n\nconst channelAvailability = ({ ip, originalUrl, params: { name } }, res) => {\n  const gaStartTime = Date.now();\n  name = addAtSymbolIfNecessary(name);\n  checkChannelAvailability(name)\n    .then(isAvailable => {\n      let responseObject = {\n        success: true,\n        data   : isAvailable,\n      };\n      if (isAvailable) {\n        responseObject['message'] = `${name} is available`;\n      } else {\n        responseObject['message'] = `${name} is already in use`;\n      }\n      res.status(200).json(responseObject);\n      sendGATimingEvent('end-to-end', 'channel name availability', name, gaStartTime, Date.now());\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = channelAvailability;\n"
  },
  {
    "path": "server/controllers/api/channel/claims/channelPagination.js",
    "content": "const CLAIMS_PER_PAGE = 12;\n\nmodule.exports = {\n  returnPaginatedChannelClaims (channelName, longChannelClaimId, claims, page) {\n    const totalPages = module.exports.determineTotalPages(claims);\n    const paginationPage = module.exports.getPageFromQuery(page);\n    return {\n      channelName       : channelName,\n      longChannelClaimId: longChannelClaimId,\n      claims            : module.exports.extractPageFromClaims(claims, paginationPage),\n      previousPage      : module.exports.determinePreviousPage(paginationPage),\n      currentPage       : paginationPage,\n      nextPage          : module.exports.determineNextPage(totalPages, paginationPage),\n      totalPages        : totalPages,\n      totalResults      : module.exports.determineTotalClaims(claims),\n    };\n  },\n  getPageFromQuery (page) {\n    if (page) {\n      return parseInt(page);\n    }\n    return 1;\n  },\n  extractPageFromClaims (claims, pageNumber) {\n    if (!claims) {\n      return [];  // if no claims, return this default\n    }\n    // logger.debug('claims is array?', Array.isArray(claims));\n    // logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber));\n    const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE;\n    const claimEndIndex = claimStartIndex + CLAIMS_PER_PAGE;\n    return claims.slice(claimStartIndex, claimEndIndex);\n  },\n  determineTotalPages (claims) {\n    if (!claims) {\n      return 0;\n    } else {\n      const totalClaims = claims.length;\n      if (totalClaims < CLAIMS_PER_PAGE) {\n        return 1;\n      }\n      const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE);\n      const remainder = totalClaims % CLAIMS_PER_PAGE;\n      if (remainder === 0) {\n        return fullPages;\n      }\n      return fullPages + 1;\n    }\n  },\n  determinePreviousPage (currentPage) {\n    if (currentPage === 1) {\n      return null;\n    }\n    return currentPage - 1;\n  },\n  determineNextPage (totalPages, currentPage) {\n    if (currentPage === totalPages) {\n      return null;\n    }\n    return currentPage + 1;\n  },\n  determineTotalClaims (claims) {\n    if (!claims) {\n      return 0;\n    }\n    return claims.length;\n  },\n};\n"
  },
  {
    "path": "server/controllers/api/channel/claims/getChannelClaims.js",
    "content": "const chainquery = require('chainquery').default;\nconst logger = require('winston');\nconst getClaimData = require('server/utils/getClaimData');\nconst { returnPaginatedChannelClaims } = require('./channelPagination.js');\n\nconst getChannelClaims = async (channelName, channelLongId, page) => {\n  logger.debug(`getChannelClaims: ${channelName}, ${channelLongId}, ${page}`);\n  let channelShortId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(\n    channelLongId,\n    channelName\n  );\n  let channelClaims;\n  if (channelLongId) {\n    channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelLongId);\n  }\n  /*\n    Put mempool unconfirmed claims at the beginning\n   */\n  const split = channelClaims.reduce(\n    (acc, val) =>\n      val.dataValues.height === 0\n        ? { ...acc, zero: acc.zero.concat(val) }\n        : { ...acc, nonzero: acc.nonzero.concat(val) },\n    { zero: [], nonzero: [] }\n  );\n  channelClaims = split.zero.concat(split.nonzero);\n\n  const processingChannelClaims = channelClaims\n    ? channelClaims.map(claim => getClaimData(claim, channelName, channelShortId))\n    : [];\n  const processedChannelClaims = await Promise.all(processingChannelClaims);\n\n  return returnPaginatedChannelClaims(channelName, channelShortId, processedChannelClaims, page);\n};\n\nmodule.exports = getChannelClaims;\n"
  },
  {
    "path": "server/controllers/api/channel/claims/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst getChannelClaims = require('./getChannelClaims.js');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\n\n/*\n\n  route to get all claims for channel\n\n*/\n\nconst channelClaims = ({ ip, originalUrl, body, params }, res) => {\n  const channelName = params.channelName;\n  let channelClaimId = params.channelClaimId;\n  if (channelClaimId === 'none') channelClaimId = null;\n  const page = params.page;\n  getChannelClaims(channelName, channelClaimId, page)\n    .then(data => {\n      res.status(200).json({success: true, data});\n    })\n    .catch(error => {\n      if (error === NO_CHANNEL) {\n        return res.status(404).json({\n          success: false,\n          message: 'No matching channel was found',\n        });\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = channelClaims;\n"
  },
  {
    "path": "server/controllers/api/channel/data/getChannelData.js",
    "content": "const db = require('server/models');\nconst chainquery = require('chainquery').default;\n\nconst getChannelData = async (channelName, channelClaimId) => {\n  let longChannelClaimId = await chainquery.claim.queries.getLongClaimId(channelName, channelClaimId).catch(() => false);\n\n  if (!longChannelClaimId) {\n    // Allow an error to throw here if this fails\n    longChannelClaimId = await db.Certificate.getLongChannelId(channelName, channelClaimId);\n  }\n\n  let shortChannelClaimId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(longChannelClaimId, channelName).catch(() => false);\n\n  if (!shortChannelClaimId) {\n    shortChannelClaimId = await db.Certificate.getShortChannelIdFromLongChannelId(longChannelClaimId, channelName);\n  }\n\n  return {\n    channelName,\n    longChannelClaimId,\n    shortChannelClaimId,\n  };\n};\n\nmodule.exports = getChannelData;\n"
  },
  {
    "path": "server/controllers/api/channel/data/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst getChannelData = require('./getChannelData.js');\nconst isApprovedChannel = require('../../../../../utils/isApprovedChannel');\nconst { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\nconst LONG_ID = 'longId';\nconst SHORT_ID = 'shortId';\nconst LONG_CLAIM_LENGTH = 40;\n\n/*\n\n  route to get data for a channel\n\n*/\n\nconst channelData = ({ ip, originalUrl, body, params }, res) => {\n  const channelName = params.channelName;\n  let channelClaimId = params.channelClaimId;\n  if (channelClaimId === 'none') channelClaimId = null;\n  const chanObj = {};\n  if (channelName) chanObj.name = channelName;\n  if (channelClaimId) chanObj[(channelClaimId.length === LONG_CLAIM_LENGTH ? LONG_ID : SHORT_ID)] = channelClaimId;\n  if (serveOnlyApproved && !isApprovedChannel(chanObj, approvedChannels)) {\n    return res.status(404).json({\n      success: false,\n      message: 'This content is unavailable',\n    });\n  }\n\n  getChannelData(channelName, channelClaimId)\n    .then(data => {\n      res.status(200).json({\n        success: true,\n        data,\n      });\n    })\n    .catch(error => {\n      if (error === NO_CHANNEL) {\n        return res.status(404).json({\n          success: false,\n          message: 'No matching channel was found',\n        });\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = channelData;\n"
  },
  {
    "path": "server/controllers/api/channel/shortId/index.js",
    "content": "const { handleErrorResponse } = require('server/controllers/utils/errorHandlers.js');\nconst db = require('server/models');\nconst chainquery = require('chainquery').default;\n\n/*\n\nroute to get a short channel id from long channel Id\n\n*/\n\nconst channelShortIdRoute = async ({ ip, originalUrl, params }, res) => {\n  try {\n    let shortId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(params.longId, params.name).catch(() => false);\n\n    if (!shortId) {\n      shortId = await db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name);\n    }\n\n    res.status(200).json(shortId);\n  } catch (error) {\n    handleErrorResponse(originalUrl, ip, error, res);\n  }\n};\n\nmodule.exports = channelShortIdRoute;\n"
  },
  {
    "path": "server/controllers/api/claim/abandon/index.js",
    "content": "const logger = require('winston');\nconst db = require('server/models');\nconst { abandonClaim } = require('server/lbrynet');\nconst deleteFile = require('../publish/deleteFile.js');\nconst authenticateUser = require('../publish/authentication.js');\n\n/*\n  route to abandon a claim through the daemon\n*/\n\nconst claimAbandon = async (req, res) => {\n  const { outpoint } = req.body;\n  const { user } = req;\n  try {\n    const [channel, claim] = await Promise.all([\n      authenticateUser(user.channelName, null, null, user),\n      db.Claim.findOne({ where: { outpoint } }),\n    ]);\n\n    if (!claim) throw new Error('That channel does not exist');\n    if (!channel.channelName) throw new Error(\"You don't own this channel\");\n\n    await abandonClaim({ outpoint });\n    const file = await db.File.findOne({ where: { outpoint } });\n    await Promise.all([\n      deleteFile(file.filePath),\n      db.File.destroy({ where: { outpoint } }),\n      db.Claim.destroy({ where: { outpoint } }),\n    ]);\n    logger.debug(`Claim abandoned: ${outpoint}`);\n    res.status(200).json({\n      success: true,\n      message: `Claim with outpoint ${outpoint} abandonded`,\n    });\n  } catch (error) {\n    logger.error('abandon claim error:', error);\n    res.status(400).json({\n      success: false,\n      message: error.message,\n    });\n  }\n};\n\nmodule.exports = claimAbandon;\n"
  },
  {
    "path": "server/controllers/api/claim/availability/checkClaimAvailability.js",
    "content": "const chainquery = require('chainquery').default;\nconst { publishing: { primaryClaimAddress, additionalClaimAddresses } } = require('@config/siteConfig');\nconst Sequelize = require('sequelize');\nconst Op = Sequelize.Op;\n\nconst claimAvailability = async (name) => {\n  const claimAddresses = additionalClaimAddresses || [];\n  claimAddresses.push(primaryClaimAddress);\n  // find any records where the name is used\n  return chainquery.claim\n    .findAll({\n      attributes: ['claim_address'],\n      where     : {\n        name,\n        claim_address: {\n          [Op.or]: claimAddresses,\n        },\n      },\n    })\n    .then(result => {\n      return (result.length <= 0);\n    })\n    .catch(error => {\n      throw error;\n    });\n};\n\nmodule.exports = claimAvailability;\n"
  },
  {
    "path": "server/controllers/api/claim/availability/index.js",
    "content": "const checkClaimAvailability = require('./checkClaimAvailability.js');\nconst { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\n/*\n\n  route to check whether this site published to a claim\n\n*/\n\nconst claimAvailability = ({ ip, originalUrl, params: { name } }, res) => {\n  const gaStartTime = Date.now();\n  checkClaimAvailability(name)\n    .then(isAvailable => {\n      let responseObject = {\n        success: true,\n        data   : isAvailable,\n      };\n      if (isAvailable) {\n        responseObject['message'] = `That claim name is available`;\n      } else {\n        responseObject['message'] = `That url is already in use`;\n      }\n      res.status(200).json(responseObject);\n      sendGATimingEvent('end-to-end', 'claim name availability', name, gaStartTime, Date.now());\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimAvailability;\n"
  },
  {
    "path": "server/controllers/api/claim/data/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst getClaimData = require('server/utils/getClaimData');\nconst fetchClaimData = require('server/utils/fetchClaimData');\nconst chainquery = require('chainquery').default;\nconst db = require('server/models');\n/*\n\n  route to return data for a claim\n\n*/\n\nconst claimData = async ({ ip, originalUrl, body, params }, res) => {\n  try {\n    const resolvedClaim = await fetchClaimData(params);\n\n    if (!resolvedClaim) {\n      return res.status(404).json({\n        success: false,\n        message: 'No claim could be found',\n      });\n    }\n\n    res.status(200).json({\n      success: true,\n      data   : await getClaimData(resolvedClaim),\n    });\n  } catch (error) {\n    handleErrorResponse(originalUrl, ip, error, res);\n  }\n};\n\nmodule.exports = claimData;\n"
  },
  {
    "path": "server/controllers/api/claim/get/index.js",
    "content": "const { getClaim, resolveUri } = require('server/lbrynet');\nconst { createFileRecordDataAfterGet } = require('server/models/utils/createFileRecordData.js');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst getClaimData = require('server/utils/getClaimData');\nconst chainquery = require('chainquery').default;\nconst db = require('server/models');\nconst logger = require('winston');\nconst awaitFileSize = require('server/utils/awaitFileSize');\nconst isBot = require('isbot');\n\n/*\n\n  route to get a claim\n\n*/\n\nconst claimGet = async ({ ip, originalUrl, params, headers }, res) => {\n  const name = params.name;\n  const claimId = params.claimId;\n\n  try {\n    let claimInfo = await chainquery.claim.queries.resolveClaim(name, claimId).catch(() => {});\n    if (claimInfo) {\n      logger.debug(`claim/get: claim resolved in chainquery`);\n    }\n    if (!claimInfo) {\n      claimInfo = await db.Claim.resolveClaim(name, claimId);\n    }\n    if (!claimInfo) {\n      throw new Error('claim/get: resolveClaim: No matching uri found in Claim table');\n    }\n    if (headers && headers['user-agent'] && isBot(headers['user-agent'])) {\n      logger.info(`Bot GetClaim: claimId: ${claimId}`);\n      res.status(200).json({\n        success: true,\n        message: 'bot',\n        completed: false,\n      });\n      return true;\n    }\n    logger.info(`GetClaim: ${claimId} UA: ${headers['user-agent']}`);\n    let lbrynetResult = await getClaim(`${name}#${claimId}`);\n    if (!lbrynetResult) {\n      throw new Error(`claim/get: getClaim Unable to Get ${name}#${claimId}`);\n    }\n    const claimData = await getClaimData(claimInfo);\n    if (!claimData) {\n      throw new Error('claim/get: getClaimData failed to get file blobs');\n    }\n    const fileReady = await awaitFileSize(lbrynetResult.outpoint, 10000000, 250, 10000);\n\n    if (fileReady !== 'ready') {\n      throw new Error('claim/get: failed to get file after 10 seconds');\n    }\n    const fileData = await createFileRecordDataAfterGet(claimData, lbrynetResult);\n    if (!fileData) {\n      throw new Error('claim/get: createFileRecordDataAfterGet failed to create file in time');\n    }\n    const upsertCriteria = { name, claimId };\n    await db.upsert(db.File, fileData, upsertCriteria, 'File');\n    const { message, completed } = lbrynetResult;\n    res.status(200).json({\n      success: true,\n      message,\n      completed,\n    });\n  } catch (error) {\n    handleErrorResponse(originalUrl, ip, error, res);\n  }\n};\nmodule.exports = claimGet;\n"
  },
  {
    "path": "server/controllers/api/claim/list/index.js",
    "content": "const { getClaimList } = require('../../../../lbrynet');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\n/*\n\n  route to get list of claims\n\n*/\n\nconst claimList = ({ ip, originalUrl, params }, res) => {\n  getClaimList(params.name)\n    .then(claimsList => {\n      res.status(200).json(claimsList);\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimList;\n"
  },
  {
    "path": "server/controllers/api/claim/longId/index.js",
    "content": "const db = require('server/models');\nconst chainquery = require('chainquery').default;\n\nconst { handleErrorResponse } = require('server/controllers/utils/errorHandlers.js');\n\nconst getClaimId = require('server/controllers/utils/getClaimId.js');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\nconst NO_CLAIM = 'NO_CLAIM';\nconst BLOCKED_CLAIM = 'BLOCKED_CLAIM';\n\n/*\n\n  route to get a long claim id\n\n*/\n\nconst claimLongId = ({ ip, originalUrl, body, params }, res) => {\n  const channelName = body.channelName;\n  const channelClaimId = body.channelClaimId;\n  const claimName = body.claimName;\n  let claimId = body.claimId;\n\n  getClaimId(channelName, channelClaimId, claimName, claimId)\n    .then(fullClaimId => {\n      claimId = fullClaimId;\n      return chainquery.claim.queries.getOutpoint(claimName, fullClaimId).catch(() => {});\n    })\n    .then(outpointResult => {\n      if (!outpointResult) {\n        return db.Claim.getOutpoint(claimName, claimId);\n      }\n      return outpointResult;\n    })\n    .then(outpoint => {\n      return db.Blocked.isNotBlocked(outpoint);\n    })\n    .then(() => {\n      res.status(200).json({ success: true, data: claimId });\n    })\n    .catch(error => {\n      if (error === NO_CLAIM) {\n        return res.status(404).json({\n          success: false,\n          message: 'No matching claim id could be found for that url',\n        });\n      }\n      if (error === NO_CHANNEL) {\n        return res.status(404).json({\n          success: false,\n          message: 'No matching channel id could be found for that url',\n        });\n      }\n      if (error === BLOCKED_CLAIM) {\n        return res.status(410).json({\n          success: false,\n          message:\n            'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications. For more details, see https://lbry.com/faq/dmca',\n        });\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimLongId;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/authentication.js",
    "content": "const logger = require('winston');\nconst db = require('../../../../models');\n\nconst authenticateChannelCredentials = (channelName, channelId, userPassword) => {\n  return new Promise((resolve, reject) => {\n    // hoisted variables\n    let channelData;\n    // build the params for finding the channel\n    let channelFindParams = {};\n    if (channelName) channelFindParams['channelName'] = channelName;\n    if (channelId) channelFindParams['channelClaimId'] = channelId;\n    // find the channel\n    db.Channel\n      .findOne({\n        where: channelFindParams,\n      })\n      .then(channel => {\n        if (!channel) {\n          logger.debug('no channel found');\n          throw new Error('Authentication failed, you do not have access to that channel');\n        }\n        channelData = channel.get();\n        logger.debug('channel data:', channelData);\n        return db.User.findOne({\n          where: { userName: channelData.channelName.substring(1) },\n        });\n      })\n      .then(user => {\n        if (!user) {\n          logger.debug('no user found');\n          throw new Error('Authentication failed, you do not have access to that channel');\n        }\n        return user.comparePassword(userPassword);\n      })\n      .then(isMatch => {\n        if (!isMatch) {\n          logger.debug('incorrect password');\n          throw new Error('Authentication failed, you do not have access to that channel');\n        }\n        logger.debug('...password was a match...');\n        resolve(channelData);\n      })\n      .catch(error => {\n        reject(error);\n      });\n  });\n};\n\nconst authenticateUser = (channelName, channelId, channelPassword, user) => {\n  return new Promise((resolve, reject) => {\n    // case: no channelName or channel Id are provided (anonymous), regardless of whether user token is provided\n    if (!channelName && !channelId) {\n      resolve({\n        channelName   : null,\n        channelClaimId: null,\n      });\n      return;\n    }\n    // case: channelName or channel Id are provided with user token\n    if (user) {\n      if (channelName && channelName !== user.channelName) {\n        reject(new Error('the provided channel name does not match user credentials'));\n        return;\n      }\n      if (channelId && channelId !== user.channelClaimId) {\n        reject(new Error('the provided channel id does not match user credentials'));\n        return;\n      }\n      resolve({\n        channelName   : user.channelName,\n        channelClaimId: user.channelClaimId,\n      });\n      return;\n    }\n    // case: channelName or channel Id are provided with password instead of user token\n    if (!channelPassword) {\n      reject(new Error('no channel password provided'));\n      return;\n    }\n    resolve(authenticateChannelCredentials(channelName, channelId, channelPassword));\n  });\n};\n\nmodule.exports = authenticateUser;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/createPublishParams.js",
    "content": "const logger = require('winston');\nconst { details, publishing } = require('@config/siteConfig');\nconst createPublishParams = (\n  filePath,\n  name,\n  title,\n  description,\n  license,\n  licenseUrl,\n  nsfw,\n  thumbnail,\n  channelName,\n  channelClaimId\n) => {\n  // provide defaults for title\n  if (title === null || title.trim() === '') {\n    title = name;\n  }\n  // provide default for description\n  if (description === null || description.trim() === '') {\n    description = '';\n  }\n  // provide default for license\n  if (license === null || license.trim() === '') {\n    license = ''; // default to empty string\n  }\n  // provide default for licenseUrl\n  if (licenseUrl === null || licenseUrl.trim() === '') {\n    licenseUrl = ''; // default to empty string\n  }\n  // create the basic publish params\n  const publishParams = {\n    name,\n    file_path: filePath,\n    bid: publishing.fileClaimBidAmount,\n    description,\n    title,\n    author: details.title,\n    languages: ['en'],\n    license,\n    license_url: licenseUrl,\n    tags: [],\n    claim_address: publishing.primaryClaimAddress,\n  };\n  // add thumbnail to channel if video\n  if (thumbnail) {\n    publishParams['thumbnail_url'] = thumbnail;\n  }\n  if (nsfw) {\n    publishParams.tags = ['mature'];\n  }\n  // add channel details if publishing to a channel\n  if (channelName && channelClaimId) {\n    publishParams['channel_id'] = channelClaimId;\n    publishParams['channel_name'] = channelName;\n  }\n  // log params\n  logger.debug('publish params:', publishParams);\n  // return\n  return publishParams;\n};\n\nmodule.exports = createPublishParams;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/createThumbnailPublishParams.js",
    "content": "const logger = require('winston');\nconst { details, publishing } = require('@config/siteConfig');\n\nconst createThumbnailPublishParams = (thumbnailFilePath, claimName, license, licenseUrl, nsfw) => {\n  if (!thumbnailFilePath) {\n    return;\n  }\n  logger.debug(`Creating Thumbnail Publish Parameters`);\n  // create the publish params\n\n  if (license === null || license.trim() === '') {\n    license = ''; // default to empty string\n  }\n  // provide default for licenseUrl\n  if (licenseUrl === null || licenseUrl.trim() === '') {\n    licenseUrl = ''; // default to empty string\n  }\n\n  return {\n    name: `${claimName}-thumb`,\n    file_path: thumbnailFilePath,\n    bid: publishing.fileClaimBidAmount,\n    title: `${claimName} thumbnail`,\n    description: `a thumbnail for ${claimName}`,\n    author: details.title,\n    languages: ['en'],\n    license,\n    license_url: licenseUrl,\n    claim_address: publishing.primaryClaimAddress,\n    channel_name: publishing.thumbnailChannel,\n    channel_id: publishing.thumbnailChannelId,\n  };\n};\n\nmodule.exports = createThumbnailPublishParams;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/deleteFile.js",
    "content": "const logger = require('winston');\nconst fs = require('fs');\n\nconst deleteFile = (filePath) => {\n  fs.unlink(filePath, err => {\n    if (err) {\n      return logger.error(`error deleting temporary file ${filePath}`);\n    }\n    logger.info(`successfully deleted ${filePath}`);\n  });\n};\n\nmodule.exports = deleteFile;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/index.js",
    "content": "const logger = require('winston');\n\nconst {\n  details: { host },\n  publishing: { disabled, disabledMessage },\n} = require('@config/siteConfig');\n\nconst { sendGATimingEvent } = require('server/utils/googleAnalytics.js');\nconst isApprovedChannel = require('@globalutils/isApprovedChannel');\nconst {\n  publishing: { publishOnlyApproved, approvedChannels },\n} = require('@config/siteConfig');\n\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\nconst checkClaimAvailability = require('../availability/checkClaimAvailability.js');\n\nconst publish = require('./publish.js');\nconst createPublishParams = require('./createPublishParams.js');\nconst createThumbnailPublishParams = require('./createThumbnailPublishParams.js');\nconst parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');\nconst parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');\nconst authenticateUser = require('./authentication.js');\n\nconst chainquery = require('chainquery').default;\nconst createCanonicalLink = require('@globalutils/createCanonicalLink');\n\nconst CLAIM_TAKEN = 'CLAIM_TAKEN';\nconst UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';\n\n/*\n\n  route to publish a claim through the daemon\n\n*/\n\nconst claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {\n  // logging\n  logger.info('Publish request:', {\n    ip,\n    headers,\n    body,\n    files,\n  });\n  // check for disabled publishing\n  if (disabled) {\n    return res.status(503).json({\n      success: false,\n      message: disabledMessage,\n    });\n  }\n  // define variables\n  let channelName,\n    channelId,\n    channelPassword,\n    description,\n    fileName,\n    filePath,\n    fileExtension,\n    fileType,\n    gaStartTime,\n    license,\n    licenseUrl,\n    name,\n    nsfw,\n    thumbnail,\n    thumbnailFileName,\n    thumbnailFilePath,\n    thumbnailFileType,\n    title,\n    claimData,\n    claimId;\n  // record the start time of the request\n  gaStartTime = Date.now();\n  // validate the body and files of the request\n  try {\n    // validateApiPublishRequest(body, files);\n    ({\n      name,\n      nsfw,\n      license,\n      licenseUrl,\n      title,\n      description,\n      thumbnail,\n    } = parsePublishApiRequestBody(body));\n    ({\n      fileName,\n      filePath,\n      fileExtension,\n      fileType,\n      thumbnailFileName,\n      thumbnailFilePath,\n      thumbnailFileType,\n    } = parsePublishApiRequestFiles(files));\n    ({ channelName, channelId, channelPassword } = body);\n  } catch (error) {\n    return res.status(400).json({ success: false, message: error.message });\n  }\n  // check channel authorization\n  authenticateUser(channelName, channelId, channelPassword, user)\n    .then(({ channelName, channelClaimId }) => {\n      if (publishOnlyApproved && !isApprovedChannel({ longId: channelClaimId }, approvedChannels)) {\n        const error = {\n          name: UNAPPROVED_CHANNEL,\n          message: 'This spee.ch instance only allows publishing to approved channels',\n        };\n        throw error;\n      }\n\n      return Promise.all([\n        checkClaimAvailability(name),\n        createPublishParams(\n          filePath,\n          name,\n          title,\n          description,\n          license,\n          licenseUrl,\n          nsfw,\n          thumbnail,\n          channelName,\n          channelClaimId\n        ),\n        createThumbnailPublishParams(thumbnailFilePath, name, license, licenseUrl, nsfw),\n      ]);\n    })\n    .then(([claimAvailable, publishParams, thumbnailPublishParams]) => {\n      if (!claimAvailable) {\n        const error = {\n          name: CLAIM_TAKEN,\n          message: 'That claim name is already taken',\n        };\n        throw error;\n      }\n      // publish the thumbnail, if one exists\n      if (thumbnailPublishParams) {\n        publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);\n      }\n      // publish the asset\n      return publish(publishParams, fileName, fileType, filePath);\n    })\n    .then(publishResults => {\n      logger.debug('Publish success >', publishResults);\n      claimData = publishResults;\n      ({ claimId } = claimData);\n\n      if (channelName) {\n        logger.debug(`api/claim/publish: claimData.certificateId ${claimData.certificateId}`);\n        return chainquery.claim.queries.getShortClaimIdFromLongClaimId(\n          claimData.certificateId,\n          channelName\n        );\n      } else {\n        return chainquery.claim.queries\n          .getShortClaimIdFromLongClaimId(claimId, name, claimData)\n          .catch(() => {\n            return claimId.slice(0, 1);\n          });\n      }\n    })\n    .then(shortId => {\n      let canonicalUrl;\n      if (channelName) {\n        canonicalUrl = createCanonicalLink({ asset: { ...claimData, channelShortId: shortId } });\n      } else {\n        canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId } });\n      }\n\n      res.status(200).json({\n        success: true,\n        message: 'publish completed successfully',\n        data: {\n          name,\n          claimId,\n          url: `${host}${canonicalUrl}`, // for backwards compatability with app\n          showUrl: `${host}${canonicalUrl}`,\n          serveUrl: `${host}${canonicalUrl}${fileExtension}`,\n          pushTo: canonicalUrl,\n          claimData,\n        },\n      });\n      // record the publish end time and send to google analytics\n      sendGATimingEvent('end-to-end', 'publish', fileType, gaStartTime, Date.now());\n    })\n    .catch(error => {\n      if ([CLAIM_TAKEN, UNAPPROVED_CHANNEL].includes(error.name)) {\n        res.status(400).json({\n          success: false,\n          message: error.message,\n        });\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimPublish;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/parsePublishApiRequestBody.js",
    "content": "const parsePublishApiRequestBody = ({\n  name,\n  nsfw,\n  license,\n  licenseUrl,\n  title,\n  description,\n  thumbnail,\n}) => {\n  // validate name\n  if (!name) {\n    throw new Error('no name field found in request');\n  }\n  const invalidNameCharacters = /[^A-Za-z0-9,-]/.exec(name);\n  if (invalidNameCharacters) {\n    throw new Error(\n      'The claim name you provided is not allowed.  Only the following characters are allowed: A-Z, a-z, 0-9, and \"-\"'\n    );\n  }\n  // optional parameters\n  nsfw = nsfw === 'true';\n  license = license || null;\n  licenseUrl = licenseUrl || null;\n  title = title || null;\n  description = description || null;\n  thumbnail = thumbnail || null;\n  // return results\n  return {\n    name,\n    nsfw,\n    license,\n    licenseUrl,\n    title,\n    description,\n    thumbnail,\n  };\n};\n\nmodule.exports = parsePublishApiRequestBody;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/parsePublishApiRequestBody.test.js",
    "content": "const chai = require('chai');\nconst expect = chai.expect;\n\ndescribe('#parsePublishApiRequestBody()', function () {\n  const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');\n\n  it('should throw an error if no body', function () {\n    expect(parsePublishApiRequestBody.bind(this, null)).to.throw();\n  });\n\n  it('should throw an error if no body.name', function () {\n    const bodyNoName = {};\n    expect(parsePublishApiRequestBody.bind(this, bodyNoName)).to.throw();\n  });\n});\n"
  },
  {
    "path": "server/controllers/api/claim/publish/parsePublishApiRequestFiles.js",
    "content": "const path = require('path');\nconst validateFileTypeAndSize = require('./validateFileTypeAndSize.js');\nconst validateFileForPublish = require('./validateFileForPublish.js');\n\nconst parsePublishApiRequestFiles = ({ file, thumbnail }, isUpdate) => {\n  // make sure a file was provided\n  if (!file) {\n    if (isUpdate) {\n      if (thumbnail) {\n        const obj = {};\n        obj.thumbnailFileName = thumbnail.name;\n        obj.thumbnailFilePath = thumbnail.path;\n        obj.thumbnailFileType = thumbnail.type;\n        return obj;\n      }\n      return {};\n    }\n    throw new Error('No file with key of [file] found in request');\n  }\n  if (!file.path) {\n    throw new Error('No file path found');\n  }\n  if (!file.type) {\n    throw new Error('No file type found');\n  }\n  if (!file.size) {\n    throw new Error('No file size found');\n  }\n  // validate the file name\n  if (!file.name) {\n    throw new Error('No file name found');\n  }\n  if (file.name.indexOf('.') < 0) {\n    throw new Error('No file extension found in file name');\n  }\n  if (file.name.indexOf('.') === 0) {\n    throw new Error('File name cannot start with \".\"');\n  }\n  if (/'/.test(file.name)) {\n    throw new Error('Apostrophes are not allowed in the file name');\n  }\n\n  // validate the file\n  if (file) validateFileForPublish(file);\n  // return results\n  const obj = {\n    fileName: file.name,\n    filePath: file.path,\n    fileExtension: path.extname(file.path),\n    fileType: file.type,\n  };\n\n  if (thumbnail) {\n    obj.thumbnailFileName = thumbnail.name;\n    obj.thumbnailFilePath = thumbnail.path;\n    obj.thumbnailFileType = thumbnail.type;\n  }\n\n  return obj;\n};\n\nmodule.exports = parsePublishApiRequestFiles;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/parsePublishApiRequestFiles.test.js",
    "content": "const chai = require('chai');\nconst expect = chai.expect;\n\ndescribe('#parsePublishApiRequestFiles()', function () {\n  const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');\n\n  it('should throw an error if no files', function () {\n    expect(parsePublishApiRequestFiles.bind(this, null)).to.throw();\n  });\n\n  it('should throw an error if no files.file', function () {\n    const filesNoFile = {};\n    expect(parsePublishApiRequestFiles.bind(this, filesNoFile)).to.throw();\n  });\n\n  it('should throw an error if file.size is too large', function () {\n    const filesTooBig = {\n      file: {\n        name: 'file.jpg',\n        path: '/path/to/file.jpg',\n        type: 'image/jpg',\n        size: 10000001,\n      },\n    };\n    expect(parsePublishApiRequestFiles.bind(this, filesTooBig)).to.throw();\n  });\n\n  it('should throw error if not an accepted file type', function () {\n    const filesWrongType = {\n      file: {\n        name: 'file.jpg',\n        path: '/path/to/file.jpg',\n        type: 'someType/ext',\n        size: 10000000,\n      },\n    };\n    expect(parsePublishApiRequestFiles.bind(this, filesWrongType)).to.throw();\n  });\n\n  it('should throw NO error if no problems', function () {\n    const filesNoProblems = {\n      file: {\n        name: 'file.jpg',\n        path: '/path/to/file.jpg',\n        type: 'image/jpg',\n        size: 10000000,\n      },\n    };\n    expect(parsePublishApiRequestFiles.bind(this, filesNoProblems)).to.not.throw();\n  });\n});\n"
  },
  {
    "path": "server/controllers/api/claim/publish/publish.js",
    "content": "const logger = require('winston');\nconst db = require('server/models');\nconst { publishClaim } = require('server/lbrynet');\nconst { createFileRecordDataAfterPublish } = require('server/models/utils/createFileRecordData.js');\nconst {\n  createClaimRecordDataAfterPublish,\n} = require('server/models/utils/createClaimRecordData.js');\nconst deleteFile = require('./deleteFile.js');\n\nconst publish = async (publishParams, fileName, fileType) => {\n  let publishResults;\n  let channel;\n  let fileRecord;\n  let newFile = Boolean(publishParams.file_path);\n  let publishResultsOutput;\n\n  try {\n    publishResults = await publishClaim(publishParams);\n    publishResultsOutput = publishResults && publishResults.outputs && publishResults.outputs[0];\n\n    logger.verbose(`Successfully published ${publishParams.name} ${fileName}`, publishResults);\n    const outpoint = `${publishResultsOutput.txid}:${publishResultsOutput.nout}`;\n    // get the channel information\n    // if (publishParams.channel_name) {\n    //   logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);\n    //   channel = await db.Channel.findOne({\n    //     where: {\n    //       channelName: publishParams.channel_name,\n    //     },\n    //   });\n    // } else {\n    //   channel = null;\n    // }\n\n    const certificateId = publishResultsOutput.signing_channel\n      ? publishResultsOutput.signing_channel.claim_id\n      : null;\n    const channelName = publishResultsOutput.signing_channel\n      ? publishResultsOutput.signing_channel.name\n      : null;\n\n    const claimRecord = await createClaimRecordDataAfterPublish(\n      certificateId,\n      channelName,\n      fileName,\n      fileType,\n      publishParams,\n      publishResultsOutput\n    );\n    const { claimId } = claimRecord;\n    const upsertCriteria = { name: publishParams.name, claimId };\n    if (newFile) {\n      // this is the problem\n      //\n      fileRecord = await createFileRecordDataAfterPublish(\n        fileName,\n        fileType,\n        publishParams,\n        publishResultsOutput\n      );\n    } else {\n      fileRecord = await db.File.findOne({ where: { claimId } }).then(result => result.dataValues);\n    }\n\n    const [file, claim] = await Promise.all([\n      db.upsert(db.File, fileRecord, upsertCriteria, 'File'),\n      db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),\n    ]);\n    logger.debug(`File and Claim records successfully created (${publishParams.name})`);\n\n    await Promise.all([file.setClaim(claim), claim.setFile(file)]);\n    logger.debug(`File and Claim records successfully associated (${publishParams.name})`);\n\n    return Object.assign({}, claimRecord, { outpoint });\n  } catch (err) {\n    // parse daemon response when err is a string\n    // this needs work\n    logger.error('publish/publish err:', err);\n    const error = typeof err === 'string' ? JSON.parse(err) : err;\n    if (publishParams.file_path) {\n      await deleteFile(publishParams.file_path);\n    }\n    const message =\n      error.error && error.error.message ? error.error.message : 'Unknown publish error';\n    return {\n      error: true,\n      message,\n    };\n  }\n};\n\nmodule.exports = publish;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/validateFileForPublish.js",
    "content": "const logger = require('winston');\n\nconst { publishing } = require('@config/siteConfig.json');\n\nconst { fileSizeLimits } = publishing;\n\nconst SIZE_MB = 1000000;\n\nconst validateFileForPublish = file => {\n  let contentType = file.type;\n  let mediaType = contentType ? contentType.substr(0, contentType.indexOf('/')) : '';\n  let mediaTypeLimit = fileSizeLimits[mediaType] || false;\n  let customLimits = fileSizeLimits['customByContentType'];\n\n  if (!file) {\n    throw new Error('no file provided');\n  }\n\n  if (/'/.test(file.name)) {\n    throw new Error('apostrophes are not allowed in the file name');\n  }\n\n  if (Object.keys(customLimits).includes(contentType)) {\n    if (file.size > customLimits[contentType]) {\n      throw new Error(\n        `Sorry, type ${contentType} is limited to ${customLimits[contentType] / SIZE_MB} MB.`\n      );\n    }\n  }\n  if (mediaTypeLimit) {\n    if (file.size > mediaTypeLimit) {\n      throw new Error(`Sorry, type ${mediaType} is limited to ${mediaTypeLimit / SIZE_MB} MB.`);\n    }\n  }\n  return file;\n};\n\nmodule.exports = validateFileForPublish;\n"
  },
  {
    "path": "server/controllers/api/claim/publish/validateFileTypeAndSize.js",
    "content": "const logger = require('winston');\n\nconst {\n  publishing: { maxSizeImage = 10000000, maxSizeGif = 50000000, maxSizeVideo = 50000000 },\n} = require('@config/siteConfig');\n\nconst SIZE_MB = 1000000;\n\nconst validateFileTypeAndSize = file => {\n  // check file type and size\n  switch (file.type) {\n    case 'image/jpeg':\n    case 'image/jpg':\n    case 'image/png':\n    case 'image/svg+xml':\n      if (file.size > maxSizeImage) {\n        logger.debug('publish > file validation > .jpeg/.jpg/.png was too big');\n        throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`);\n      }\n      break;\n    case 'image/gif':\n      if (file.size > maxSizeGif) {\n        logger.debug('publish > file validation > .gif was too big');\n        throw new Error(`Sorry, .gifs are limited to ${maxSizeGif / SIZE_MB} megabytes.`);\n      }\n      break;\n    case 'video/mp4':\n      if (file.size > maxSizeVideo) {\n        logger.debug('publish > file validation > .mp4 was too big');\n        throw new Error(`Sorry, videos are limited to ${maxSizeVideo / SIZE_MB} megabytes.`);\n      }\n      break;\n    default:\n      logger.debug('publish > file validation > unrecognized file type');\n      throw new Error(\n        'The ' +\n          file.type +\n          ' content type is not supported.  Only, image/jpg, image/png, image/gif, and video/mp4 content types are currently supported.'\n      );\n  }\n  return file;\n};\n\nmodule.exports = validateFileTypeAndSize;\n"
  },
  {
    "path": "server/controllers/api/claim/resolve/index.js",
    "content": "const { resolveUri } = require('../../../../lbrynet/index');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\n/*\n\n  route to run a resolve request on the daemon\n\n*/\n\nconst claimResolve = ({ headers, ip, originalUrl, params }, res) => {\n  resolveUri(`${params.name}#${params.claimId}`)\n    .then(resolvedUri => {\n      res.status(200).json(resolvedUri);\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimResolve;\n"
  },
  {
    "path": "server/controllers/api/claim/shortId/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst db = require('../../../../models');\nconst chainquery = require('chainquery').default;\n\n/*\n\n  route to get a short claim id from long claim Id\n\n*/\n\nconst claimShortId = async ({ ip, originalUrl, body, params }, res) => {\n  try {\n    let shortId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(params.longId, params.name).catch(() => {});\n\n    if (!shortId) {\n      shortId = await db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name);\n    }\n\n    res.status(200).json({success: true, data: shortId});\n  } catch (error) {\n    handleErrorResponse(originalUrl, ip, error, res);\n  }\n};\n\nmodule.exports = claimShortId;\n"
  },
  {
    "path": "server/controllers/api/claim/update/index.js",
    "content": "const logger = require('winston');\nconst db = require('server/models');\nconst {\n  details,\n  publishing: { disabled, disabledMessage, primaryClaimAddress },\n} = require('@config/siteConfig');\nconst { resolveUri } = require('server/lbrynet');\nconst { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst publish = require('../publish/publish.js');\nconst parsePublishApiRequestBody = require('../publish/parsePublishApiRequestBody');\nconst parsePublishApiRequestFiles = require('../publish/parsePublishApiRequestFiles.js');\nconst authenticateUser = require('../publish/authentication.js');\nconst createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');\nconst chainquery = require('chainquery').default;\nconst createCanonicalLink = require('@globalutils/createCanonicalLink');\n\n/*\n  route to update a claim through the daemon\n*/\n\nconst updateMetadata = ({ nsfw, license, licenseUrl, title, description }) => {\n  const update = {};\n  if (nsfw) update['nsfw'] = nsfw;\n  if (license) update['license'] = license;\n  if (licenseUrl) update['licenseUrl'] = licenseUrl;\n  if (title) update['title'] = title;\n  if (description) update['description'] = description;\n  return update;\n};\n\nconst rando = () => {\n  let text = '';\n  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  for (let i = 0; i < 6; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));\n  return text;\n};\n\nconst claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {\n  // logging\n  logger.debug('Claim update request:', {\n    ip,\n    headers,\n    body,\n    files,\n    user,\n  });\n\n  // check for disabled publishing\n  if (disabled) {\n    return res.status(503).json({\n      success: false,\n      message: disabledMessage,\n    });\n  }\n\n  // define variables\n  let channelName,\n    channelId,\n    channelPassword,\n    description,\n    fileName,\n    filePath,\n    fileType,\n    gaStartTime,\n    thumbnail,\n    fileExtension,\n    license,\n    licenseUrl,\n    name,\n    nsfw,\n    thumbnailFileName,\n    thumbnailFilePath,\n    thumbnailFileType,\n    title,\n    claimRecord,\n    metadata,\n    publishResult,\n    thumbnailUpdate = false;\n  // record the start time of the request\n  gaStartTime = Date.now();\n\n  try {\n    ({\n      name,\n      nsfw,\n      license,\n      licenseUrl,\n      title,\n      description,\n      thumbnail,\n    } = parsePublishApiRequestBody(body));\n    ({\n      fileName,\n      filePath,\n      fileExtension,\n      fileType,\n      thumbnailFileName,\n      thumbnailFilePath,\n      thumbnailFileType,\n    } = parsePublishApiRequestFiles(files, true));\n    ({ channelName, channelId, channelPassword } = body);\n  } catch (error) {\n    return res.status(400).json({ success: false, message: error.message });\n  }\n\n  // check channel authorization\n  authenticateUser(channelName, channelId, channelPassword, user)\n    .then(({ channelName, channelClaimId }) => {\n      if (!channelId) {\n        channelId = channelClaimId;\n      }\n      return chainquery.claim.queries\n        .resolveClaimInChannel(name, channelClaimId)\n        .then(claim => claim.dataValues);\n    })\n    .then(claim => {\n      claimRecord = claim;\n      if (claimRecord.content_type === 'video/mp4' && files.file) {\n        thumbnailUpdate = true;\n      }\n\n      if (!files.file || thumbnailUpdate) {\n        return Promise.all([\n          db.File.findOne({ where: { name, claimId: claim.claim_id } }),\n          resolveUri(`${claim.name}#${claim.claim_id}`),\n        ]);\n      }\n\n      return [null, null];\n    })\n    .then(([fileResult, resolution]) => {\n      metadata = Object.assign(\n        {},\n        {\n          title: claimRecord.title,\n          description: claimRecord.description,\n          nsfw: claimRecord.nsfw,\n          license: claimRecord.license,\n          licenseUrl: claimRecord.license_url,\n          languages: ['en'],\n          author: details.title,\n        },\n        updateMetadata({ title, description, nsfw, license, licenseUrl })\n      );\n      const publishParams = {\n        name,\n        bid: '0.01',\n        claim_address: primaryClaimAddress,\n        channel_name: channelName,\n        channel_id: channelId,\n        title,\n        description,\n        author: details.title,\n        languages: ['en'],\n        license: license || '',\n        license_url: licenseUrl || '',\n        tags: [],\n      };\n\n      if (nsfw) {\n        publishParams.tags = ['mature'];\n      }\n\n      if (files.file) {\n        if (thumbnailUpdate) {\n          // publish new thumbnail\n          const newThumbnailName = `${name}-${rando()}`;\n          const newThumbnailParams = createThumbnailPublishParams(\n            filePath,\n            newThumbnailName,\n            license,\n            nsfw\n          );\n          newThumbnailParams['file_path'] = filePath;\n          publish(newThumbnailParams, fileName, fileType);\n\n          publishParams['thumbnail'] = `${details.host}/${newThumbnailParams.channel_name}:${\n            newThumbnailParams.channel_id\n          }/${newThumbnailName}-thumb.jpg`;\n        } else {\n          publishParams['file_path'] = filePath;\n        }\n      } else {\n        fileName = fileResult.fileName;\n        fileType = fileResult.fileType;\n        publishParams['thumbnail'] = claimRecord.thumbnail_url;\n      }\n\n      const fp = files && files.file && files.file.path ? files.file.path : undefined;\n      return publish(publishParams, fileName, fileType, fp);\n    })\n    .then(result => {\n      publishResult = result;\n\n      if (channelName) {\n        return chainquery.claim.queries.getShortClaimIdFromLongClaimId(\n          publishResult.certificateId,\n          channelName\n        );\n      } else {\n        return chainquery.claim.queries\n          .getShortClaimIdFromLongClaimId(publishResult.claimId, name, publishResult)\n          .catch(() => {\n            return publishResult.claimId.slice(0, 1);\n          });\n      }\n    })\n    .then(shortId => {\n      let canonicalUrl;\n      if (channelName) {\n        canonicalUrl = createCanonicalLink({\n          asset: { ...publishResult, channelShortId: shortId },\n        });\n      } else {\n        canonicalUrl = createCanonicalLink({ asset: { ...publishResult, shortId } });\n      }\n\n      if (publishResult.error) {\n        res.status(400).json({\n          success: false,\n          message: publishResult.message,\n        });\n      }\n\n      const { claimId } = publishResult;\n      res.status(200).json({\n        success: true,\n        message: 'update successful',\n        data: {\n          name,\n          claimId,\n          url: `${details.host}${canonicalUrl}`, // for backwards compatability with app\n          showUrl: `${details.host}${canonicalUrl}`,\n          serveUrl: `${details.host}${canonicalUrl}${fileExtension}`,\n          pushTo: canonicalUrl,\n          claimData: publishResult,\n        },\n      });\n      // record the publish end time and send to google analytics\n      sendGATimingEvent('end-to-end', 'update', fileType, gaStartTime, Date.now());\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = claimUpdate;\n"
  },
  {
    "path": "server/controllers/api/claim/views/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst db = require('server/models');\n\n/*\n\n  route to return data for a claim\n\n*/\n\nconst claimViews = async ({ ip, originalUrl, body, params }, res) => {\n  let claimId = params.claimId;\n  if (claimId === 'none') claimId = null;\n\n  try {\n    const viewCount = await db.Views.getGetUniqueViewsbByClaimId(claimId);\n\n    res.status(200).json({\n      success: true,\n      data   : {\n        [claimId]: viewCount,\n      },\n    });\n  } catch (error) {\n    handleErrorResponse(originalUrl, ip, error, res);\n  }\n};\n\nmodule.exports = claimViews;\n"
  },
  {
    "path": "server/controllers/api/file/availability/index.js",
    "content": "const logger = require('winston');\n\nconst { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst { getFileListFileByOutpoint } = require('server/lbrynet');\n\nconst chainquery = require('chainquery').default;\n\n/*\n\n  route to see if asset is available locally\n\n*/\n\nconst fileAvailability = ({ ip, originalUrl, params }, res) => {\n  const name = params.name;\n  const claimId = params.claimId;\n  logger.verbose(`fileAvailability params: name:${name} claimId:${claimId}`);\n  // TODO: we probably eventually want to check the publishCache for the claimId too,\n  //  and shop the outpoint to file_list.\n  return chainquery.claim.queries\n    .resolveClaim(name, claimId)\n    .then(result => {\n      return `${result.dataValues.transaction_hash_id}:${result.dataValues.vout}`;\n    })\n    .then(outpoint => {\n      logger.debug(`fileAvailability: outpoint: ${outpoint}`);\n      return getFileListFileByOutpoint(outpoint);\n    })\n    .then(result => {\n      if (result && result[0]) {\n        return res.status(200).json({ success: true, data: true });\n      } else {\n        res.status(200).json({ success: true, data: false });\n      }\n    })\n    .catch(error => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = fileAvailability;\n"
  },
  {
    "path": "server/controllers/api/homepage/data/getChannelData.js",
    "content": "const db = require('../../../../models');\n\nconst getChannelData = (channelName, channelClaimId) => {\n  return new Promise((resolve, reject) => {\n    let longChannelClaimId;\n    // 1. get the long channel Id (make sure channel exists)\n    db.Certificate\n      .getLongChannelId(channelName, channelClaimId)\n      .then(fullClaimId => {\n        longChannelClaimId = fullClaimId;\n        return db\n          .Certificate\n          .getShortChannelIdFromLongChannelId(fullClaimId, channelName);\n      })\n      .then(shortChannelClaimId => {\n        resolve({\n          channelName,\n          longChannelClaimId,\n          shortChannelClaimId,\n        });\n      })\n      .catch(error => {\n        reject(error);\n      });\n  });\n};\n\nmodule.exports = getChannelData;\n"
  },
  {
    "path": "server/controllers/api/homepage/data/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\n\nconst getChannelData = require('./getChannelData.js');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\n\n/*\n\n  route to get data for a channel\n\n*/\n\nconst channelData = ({ ip, originalUrl, body, params }, res) => {\n  const channelName = params.channelName;\n  let channelClaimId = params.channelClaimId;\n  if (channelClaimId === 'none') channelClaimId = null;\n  getChannelData(channelName, channelClaimId)\n    .then(data => {\n      res.status(200).json({\n        success: true,\n        data,\n      });\n    })\n    .catch(error => {\n      if (error === NO_CHANNEL) {\n        return res.status(404).json({\n          success: false,\n          message: 'No matching channel was found',\n        });\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = channelData;\n"
  },
  {
    "path": "server/controllers/api/oEmbed/getOEmbedDataForAsset.js",
    "content": "const logger = require('winston');\nconst db = require('../../../models');\nconst getClaimId = require('../../utils/getClaimId');\n\nconst {\n  details: { host, title: siteTitle },\n} = require('@config/siteConfig');\n\nconst getOEmbedDataForAsset = (channelName, channelClaimId, claimName, claimId) => {\n  let fileData, claimData;\n  let data = {\n    version: '1.0',\n    provider_name: siteTitle,\n    provider_url: host,\n    cache_age: 86400, // one day in seconds\n  };\n\n  return getClaimId(channelName, channelClaimId, claimName, claimId)\n    .then(fullClaimId => {\n      claimId = fullClaimId;\n      return db.Claim.findOne({\n        where: {\n          name: claimName,\n          claimId: fullClaimId,\n        },\n      });\n    })\n    .then(claimRecord => {\n      claimData = claimRecord.dataValues;\n      return db.Blocked.isNotBlocked(claimData.outpoint);\n    })\n    .then(() => {\n      return db.File.findOne({\n        where: {\n          name: claimName,\n          claimId,\n        },\n      });\n    })\n    .then(fileRecord => {\n      fileData = fileRecord.dataValues;\n      logger.debug('file data:', fileData);\n      const serveUrl = `${host}/${fileData.claimId}/${fileData.name}.${fileData.fileType.substring(\n        fileData.fileType.indexOf('/') + 1\n      )}`;\n      // set the resource type\n      if (fileData.fileType === 'video/mp4') {\n        data['type'] = 'video';\n        data['html'] = `<video width=\"100%\" controls poster=\"${\n          claimData.thumbnail\n        }\" src=\"${serveUrl}\"/></video>`;\n      } else {\n        data['type'] = 'picture';\n        data['url'] = serveUrl;\n      }\n      // get the data\n      data['title'] = claimData.title;\n      data['width'] = fileData.fileWidth || 600;\n      data['height'] = fileData.fileHeight || 400;\n      data['author_name'] = siteTitle;\n      data['author_url'] = host;\n    })\n    .then(() => {\n      return data;\n    });\n};\n\nmodule.exports = getOEmbedDataForAsset;\n"
  },
  {
    "path": "server/controllers/api/oEmbed/getOEmbedDataForChannel.js",
    "content": "const db = require('../../../models');\n\nconst {\n  details: {\n    host,\n    title: siteTitle,\n  },\n} = require('@config/siteConfig');\n\nconst getOEmbedDataForChannel = (channelName, channelClaimId) => {\n  return db.Certificate\n    .findOne({\n      where: {\n        name   : channelName,\n        claimId: channelClaimId,\n      },\n    })\n    .then(certificateRecord => {\n      const certificateData = certificateRecord.dataValues;\n      return {\n        version      : 1.0,\n        provider_name: siteTitle,\n        provider_url : host,\n        type         : 'link',\n        author_name  : certificateData.name,\n        title        : `${certificateData.name}'s channel on Spee.ch`,\n        author_url   : `${host}/${certificateData.name}:${certificateData.claimId}`,\n        cache_age    : 86400, // one day in seconds\n      };\n    });\n};\n\nmodule.exports = getOEmbedDataForChannel;\n"
  },
  {
    "path": "server/controllers/api/oEmbed/index.js",
    "content": "const logger = require('winston');\nconst lbryUri = require('../../../../utils/lbryUri');\n\nconst getOEmbedDataForChannel = require('./getOEmbedDataForChannel');\nconst getOEmbedDataForAsset = require('./getOEmbedDataForAsset');\nconst parseSpeechUrl = require('./parseSpeechUrl');\n\nconst getOEmbedData = (req, res) => {\n  const { query: { url, format } } = req;\n  logger.debug('req url', url);\n  logger.debug('req format', format);\n\n  const { paramOne, paramTwo } = parseSpeechUrl(url);\n\n  let claimName, isChannel, channelName, channelClaimId, claimId;\n\n  if (paramTwo) {\n    ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(paramOne));\n    ({ claimName } = lbryUri.parseClaim(paramTwo));\n  } else {\n    ({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(paramOne));\n    if (!isChannel) {\n      ({ claimName } = lbryUri.parseClaim(paramOne));\n    }\n  }\n\n  if (isChannel && !paramTwo) {\n    getOEmbedDataForChannel(channelName, channelClaimId)\n      .then(data => {\n        if (format === 'xml') {\n          return res.status(503).json({\n            success: false,\n            message: 'xml format is not implemented yet',\n          });\n        } else {\n          return res.status(200).json(data);\n        }\n      })\n      .catch((error) => {\n        return res.status(404).json({\n          success: false,\n          message: error,\n        });\n      });\n  } else {\n    getOEmbedDataForAsset(channelName, channelClaimId, claimName, claimId)\n      .then(data => {\n        if (format === 'xml') {\n          return res.status(503).json({\n            success: false,\n            message: 'xml format is not implemented yet',\n          });\n        } else {\n          return res.status(200).json(data);\n        }\n      })\n      .catch((error) => {\n        return res.status(404).json({\n          success: false,\n          message: error,\n        });\n      });\n  }\n};\n\nmodule.exports = getOEmbedData;\n"
  },
  {
    "path": "server/controllers/api/oEmbed/parseSpeechUrl.js",
    "content": "const logger = require('winston');\n\nconst parseSpeechUrl = (url) => {\n  const componentsRegex = new RegExp(\n    '([^:/?#]+://)' +\n    '([^/?#]*)' +\n    '(/)' +\n    '([^/?#]*)' +\n    '(/)' +\n    '([^/?#]*)'\n  );\n  const [, , , , paramOne, , paramTwo] = componentsRegex\n    .exec(url)\n    .map(match => match || null);\n\n  logger.debug(`params from speech url: ${paramOne} ${paramTwo}`);\n\n  return {\n    paramOne,\n    paramTwo,\n  };\n};\n\nmodule.exports = parseSpeechUrl;\n"
  },
  {
    "path": "server/controllers/api/special/claims/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst db = require('server/models');\nconst getClaimData = require('server/utils/getClaimData');\n\n/*\n\n  route to get all claims for special\n\n*/\n\nconst channelClaims = async ({ ip, originalUrl, body, params }, res) => {\n  const {\n    name,\n    page,\n  } = params;\n\n  if (name === 'trending') {\n    const result = await db.Trending.getTrendingClaims();\n    const claims = await Promise.all(result.map((claim) => getClaimData(claim)));\n    return res.status(200).json({\n      success: true,\n      data   : {\n        channelName       : name,\n        claims,\n        longChannelClaimId: name,\n        currentPage       : 1,\n        nextPage          : null,\n        previousPage      : null,\n        totalPages        : 1,\n        totalResults      : claims.length,\n      },\n    });\n  }\n\n  res.status(404).json({\n    success: false,\n    message: 'Feature endpoint not found',\n  });\n  handleErrorResponse(originalUrl, ip, 'Feature endpoint not found', res);\n};\n\nmodule.exports = channelClaims;\n"
  },
  {
    "path": "server/controllers/api/tor/index.js",
    "content": "const logger = require('winston');\nconst db = require('../../../models');\n\n/*\n\n  Route to update and return tor exit nodes that can connect to this ip address\n\n*/\n\nconst getTorList = (req, res) => {\n  db.Tor.refreshTable()\n    .then(result => {\n      logger.debug('number of records', result.length);\n      res.status(200).json(result);\n    })\n    .catch((error) => {\n      logger.error(error);\n      res.status(500).json({\n        success: false,\n        error,\n      });\n    });\n};\n\nmodule.exports = getTorList;\n"
  },
  {
    "path": "server/controllers/api/user/password/index.js",
    "content": "const { handleErrorResponse } = require('../../../utils/errorHandlers.js');\nconst logger = require('winston');\nconst db = require('../../../../models');\nconst { masterPassword } = require('@private/authConfig.json');\n/*\n\n  route to update a password\n\n*/\n\nconst updateUserPassword = ({ ip, originalUrl, body }, res) => {\n  let userRecord;\n  const { userName, oldPassword, newPassword } = body;\n\n  if (!masterPassword) {\n    return res.status(400).json({\n      success: false,\n      message: 'no master password set in site config',\n    });\n  }\n\n  if (!userName || !oldPassword || !newPassword) {\n    return res.status(400).json({\n      success: false,\n      message: 'body should include userName (channel name without the @), oldPassword, & newPassword',\n    });\n  }\n\n  db.User.findOne({\n    where: {\n      userName,\n    },\n  })\n    .then(user => {\n      userRecord = user;\n      if (!userRecord) {\n        throw new Error('no user found');\n      }\n      if (oldPassword === masterPassword) {\n        logger.debug('master password provided');\n        return true;\n      } else {\n        logger.debug('old password provided');\n        return userRecord.comparePassword(oldPassword);\n      }\n    })\n    .then(isMatch => {\n      if (!isMatch) {\n        throw new Error('Incorrect old password.');\n      }\n      logger.debug('Password was a match, updating password');\n      return userRecord.changePassword(newPassword);\n    })\n    .then(() => {\n      logger.debug('Password successfully updated');\n      return res.status(200).json({\n        success: true,\n        message: 'Password successfully updated',\n        oldPassword,\n        newPassword,\n      });\n    })\n    .catch((error) => {\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = updateUserPassword;\n"
  },
  {
    "path": "server/controllers/assets/constants/request_types.js",
    "content": "const SERVE = 'SERVE';\nconst SHOW = 'SHOW';\n\nmodule.exports = {\n  SERVE,\n  SHOW,\n};\n"
  },
  {
    "path": "server/controllers/assets/serveAsset/index.js",
    "content": "const { sendGAServeEvent } = require('../../../utils/googleAnalytics');\nconst getClaimIdAndServeAsset = require('../utils/getClaimIdAndServeAsset.js');\n\n/*\n\n  route to serve an asset directly\n\n*/\n\nconst serveAsset = ({ headers, ip, originalUrl, params: { claimName, claimId } }, res) => {\n  // send google analytics\n  sendGAServeEvent(headers, ip, originalUrl);\n  // get the claim Id and then serve the asset\n  getClaimIdAndServeAsset(null, null, claimName, claimId, originalUrl, ip, res, headers);\n};\n\nmodule.exports = serveAsset;\n"
  },
  {
    "path": "server/controllers/assets/serveByClaim/index.js",
    "content": "const logger = require('winston');\n\nconst { sendGAServeEvent } = require('../../../utils/googleAnalytics');\nconst handleShowRender = require('../../../render/handleShowRender').default;\n\nconst lbryUri = require('../../../../utils/lbryUri.js');\n\nconst determineRequestType = require('../utils/determineRequestType.js');\nconst getClaimIdAndServeAsset = require('../utils/getClaimIdAndServeAsset.js');\n\nconst { SHOW } = require('../constants/request_types.js');\n\n/*\n\n  route to serve an asset or the react app via the claim name only\n\n*/\n\nconst serveByClaim = (req, res) => {\n  const { headers, ip, originalUrl, params } = req;\n\n  try {\n    let isChannel, hasFileExtension, claimName;\n\n    ({ isChannel } = lbryUri.parseIdentifier(params.claim));\n    if (isChannel) {\n      logger.debug('channel request:', { headers, ip, originalUrl, params });\n      return handleShowRender(req, res);\n    }\n\n    ({ hasFileExtension } = lbryUri.parseModifier(params.claim));\n    if (determineRequestType(hasFileExtension, headers) === SHOW) {\n      logger.debug('show request:', { headers, ip, originalUrl, params });\n      return handleShowRender(req, res);\n    }\n\n    ({ claimName } = lbryUri.parseClaim(params.claim));\n    logger.debug('serve request:', { headers, ip, originalUrl, params });\n\n    getClaimIdAndServeAsset(null, null, claimName, null, originalUrl, ip, res, headers);\n\n    sendGAServeEvent(headers, ip, originalUrl);\n  } catch (error) {\n    return res.status(400).json({success: false, message: error.message});\n  }\n};\n\nmodule.exports = serveByClaim;\n"
  },
  {
    "path": "server/controllers/assets/serveByIdentifierAndClaim/index.js",
    "content": "const logger = require('winston');\n\nconst { sendGAServeEvent } = require('../../../utils/googleAnalytics');\nconst handleShowRender = require('../../../render/handleShowRender').default;\n\nconst lbryUri = require('../../../../utils/lbryUri.js');\n\nconst determineRequestType = require('../utils/determineRequestType.js');\nconst getClaimIdAndServeAsset = require('../utils/getClaimIdAndServeAsset.js');\nconst flipClaimNameAndId = require('../utils/flipClaimNameAndId.js');\n\nconst { SHOW } = require('../constants/request_types.js');\n\n/*\n\n  route to serve an asset or the react app via the claim name and an identifier\n\n*/\n\nconst serverByIdentifierAndClaim = (req, res) => {\n  const { headers, ip, originalUrl, params } = req;\n\n  try {\n    let hasFileExtension, claimName, isChannel, channelName, channelClaimId, claimId;\n\n    ({ hasFileExtension } = lbryUri.parseModifier(params.claim));\n    if (determineRequestType(hasFileExtension, headers) === SHOW) {\n      logger.debug('show request:', { headers, ip, originalUrl, params });\n      return handleShowRender(req, res);\n    }\n\n    ({ claimName } = lbryUri.parseClaim(params.claim));\n    ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(params.identifier));\n\n    if (!isChannel) {\n      [claimId, claimName] = flipClaimNameAndId(claimId, claimName);\n    }\n\n    logger.debug('serve request:', {\n      headers,\n      ip,\n      originalUrl,\n      params,\n      channelName,\n      channelClaimId,\n      claimName,\n      claimId,\n    });\n\n    getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res, headers);\n\n    sendGAServeEvent(headers, ip, originalUrl);\n  } catch (error) {\n    return res.status(400).json({success: false, message: error.message});\n  }\n};\n\nmodule.exports = serverByIdentifierAndClaim;\n"
  },
  {
    "path": "server/controllers/assets/utils/determineRequestType.js",
    "content": "const { SERVE, SHOW } = require('../constants/request_types.js');\n\nfunction clientWantsAsset ({accept, range}) {\n  const imageIsWanted = accept && accept.match(/image\\/.*/) && !accept.match(/text\\/html/);\n  const videoIsWanted = accept && accept.match(/video\\/.*/) && !accept.match(/text\\/html/);\n  return imageIsWanted || videoIsWanted;\n}\n\nconst determineRequestType = (hasFileExtension, headers) => {\n  if (hasFileExtension || clientWantsAsset(headers)) {\n    return SERVE;\n  }\n  return SHOW;\n};\n\nmodule.exports = determineRequestType;\n"
  },
  {
    "path": "server/controllers/assets/utils/flipClaimNameAndId.js",
    "content": "function isValidClaimId (claimId) {\n  return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));\n};\n\nfunction isValidShortId (claimId) {\n  return claimId.length === 1;  // it should really evaluate the short url itself\n};\n\nfunction isValidShortIdOrClaimId (input) {\n  return (isValidClaimId(input) || isValidShortId(input));\n};\n\nconst flipClaimNameAndId = (identifier, name) => {\n  // this is a patch for backwards compatability with '/name/claimId' url format\n  if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {\n    const tempName = name;\n    name = identifier;\n    identifier = tempName;\n  }\n  return [identifier, name];\n};\n\nmodule.exports = flipClaimNameAndId;\n"
  },
  {
    "path": "server/controllers/assets/utils/getClaimIdAndServeAsset.js",
    "content": "const logger = require('winston');\n\nconst db = require('../../../models');\nconst chainquery = require('chainquery').default;\nconst isApprovedChannel = require('../../../../utils/isApprovedChannel');\n\nconst getClaimId = require('../../utils/getClaimId.js');\nconst { handleErrorResponse } = require('../../utils/errorHandlers.js');\n\nconst serveFile = require('./serveFile.js');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\nconst NO_CLAIM = 'NO_CLAIM';\nconst BLOCKED_CLAIM = 'BLOCKED_CLAIM';\nconst NO_FILE = 'NO_FILE';\nconst CONTENT_UNAVAILABLE = 'CONTENT_UNAVAILABLE';\n\nconst {\n  publishing: { serveOnlyApproved, approvedChannels },\n} = require('@config/siteConfig');\n\nconst getClaimIdAndServeAsset = (\n  channelName,\n  channelClaimId,\n  claimName,\n  claimId,\n  originalUrl,\n  ip,\n  res,\n  headers\n) => {\n  getClaimId(channelName, channelClaimId, claimName, claimId)\n    .then(fullClaimId => {\n      claimId = fullClaimId;\n      return chainquery.claim.queries.resolveClaim(claimName, fullClaimId).catch(() => {});\n    })\n    .then(claim => {\n      if (!claim) {\n        logger.debug('Full claim id:', claimId);\n        return db.Claim.findOne({\n          where: {\n            name: claimName,\n            claimId,\n          },\n        });\n      }\n\n      return claim;\n    })\n    .then(claim => {\n      let claimDataValues = claim.dataValues;\n\n      if (\n        serveOnlyApproved &&\n        !isApprovedChannel(\n          { longId: claimDataValues.publisher_id || claimDataValues.certificateId },\n          approvedChannels\n        )\n      ) {\n        throw new Error(CONTENT_UNAVAILABLE);\n      }\n\n      let outpoint =\n        claimDataValues.outpoint ||\n        `${claimDataValues.transaction_hash_id}:${claimDataValues.vout}`;\n      logger.debug('Outpoint:', outpoint);\n      return db.Blocked.isNotBlocked(outpoint)\n        // .then(() => {\n        // If content was found, is approved, and not blocked - log a view.\n        // if (headers && headers['user-agent'] && /LBRY/.test(headers['user-agent']) === false) {\n        //   db.Views.create({\n        //     time: Date.now(),\n        //     isChannel: false,\n        //     claimId: claimDataValues.claim_id || claimDataValues.claimId,\n        //     publisherId: claimDataValues.publisher_id || claimDataValues.certificateId,\n        //     ip,\n        //   });\n        // }\n       // });\n    })\n    .then(() => {\n      return db.File.findOne({\n        where: {\n          claimId,\n          name: claimName,\n        },\n      });\n    })\n    .then(fileRecord => {\n      if (!fileRecord) {\n        throw NO_FILE;\n      }\n      serveFile(fileRecord.dataValues, res, originalUrl);\n    })\n    .catch(error => {\n      if (error === NO_CLAIM) {\n        logger.debug('no claim found');\n        return res.status(404).json({\n          success: false,\n          message: 'No matching claim id could be found for that url',\n        });\n      }\n      if (error === NO_CHANNEL) {\n        logger.debug('no channel found');\n        return res.status(404).json({\n          success: false,\n          message: 'No matching channel id could be found for that url',\n        });\n      }\n      if (error === CONTENT_UNAVAILABLE) {\n        logger.debug('unapproved channel');\n        return res.status(400).json({\n          success: false,\n          message: 'This content is unavailable',\n        });\n      }\n      if (error === BLOCKED_CLAIM) {\n        logger.debug('claim was blocked');\n        return res.status(451).json({\n          success: false,\n          message:\n            'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications. For more details, see https://lbry.com/faq/dmca',\n        });\n      }\n      if (error === NO_FILE) {\n        logger.debug('no file available');\n        return res.status(307).redirect(`/api/claim/get/${claimName}/${claimId}`);\n      }\n      handleErrorResponse(originalUrl, ip, error, res);\n    });\n};\n\nmodule.exports = getClaimIdAndServeAsset;\n"
  },
  {
    "path": "server/controllers/assets/utils/getLocalFileRecord.js",
    "content": "const db = require('../../../models');\n\nconst NO_FILE = 'NO_FILE';\n\nconst getLocalFileRecord = (claimId, name) => {\n  return db.File.findOne({where: {claimId, name}})\n    .then(file => {\n      if (!file) {\n        return NO_FILE;\n      }\n      return file.dataValues;\n    });\n};\n\nmodule.exports = getLocalFileRecord;\n"
  },
  {
    "path": "server/controllers/assets/utils/logRequestData.js",
    "content": "const logger = require('winston');\n\nconst logRequestData = (responseType, claimName, channelName, claimId) => {\n  logger.debug('responseType ===', responseType);\n  logger.debug('claim name === ', claimName);\n  logger.debug('channel name ===', channelName);\n  logger.debug('claim id ===', claimId);\n};\n\nmodule.exports = logRequestData;\n"
  },
  {
    "path": "server/controllers/assets/utils/serveFile.js",
    "content": "const logger = require('winston');\nconst transformImage = require('./transformImage');\n\nconst isValidQueryObject = require('server/utils/isValidQueryObj');\nconst {\n  serving: { dynamicFileSizing },\n} = require('@config/siteConfig');\nconst { enabled: dynamicEnabled } = dynamicFileSizing;\n\nconst serveFile = async ({ filePath, fileType }, res, originalUrl) => {\n  const queryObject = {};\n  // TODO: replace quick/dirty try catch with better practice\n  try {\n    originalUrl\n      .split('?')[1]\n      .split('&')\n      .map(pair => {\n        if (pair.includes('=')) {\n          let parr = pair.split('=');\n          queryObject[parr[0]] = parr[1];\n        } else queryObject[pair] = true;\n      });\n  } catch (e) {}\n\n  if (!fileType) {\n    logger.error(`no fileType provided for ${filePath}`);\n  }\n\n  let mediaType = fileType ? fileType.substr(0, fileType.indexOf('/')) : '';\n  const transform =\n    mediaType === 'image' &&\n    queryObject.hasOwnProperty('h') &&\n    queryObject.hasOwnProperty('w') &&\n    dynamicEnabled;\n\n  const sendFileOptions = {\n    headers: {\n      'X-Content-Type-Options': 'nosniff',\n      'Content-Type': fileType,\n      'Access-Control-Allow-Origin': '*',\n      'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',\n    },\n  };\n  logger.debug(`fileOptions for ${filePath}:`, sendFileOptions);\n  try {\n    if (transform) {\n      if (!isValidQueryObject(queryObject)) {\n        logger.debug(`Unacceptable querystring`, { queryObject });\n        res.status(400).json({\n          success: false,\n          message: 'Querystring may not have dimensions greater than 2000',\n        });\n        res.end();\n      }\n      logger.debug(`transforming and sending file`);\n\n      let xformed = await transformImage(filePath, queryObject);\n      res.status(200).set(sendFileOptions.headers);\n      res.end(xformed, 'binary');\n    } else {\n      res.status(200).sendFile(filePath, sendFileOptions);\n    }\n  } catch (e) {\n    logger.debug(e);\n  }\n};\n\nmodule.exports = serveFile;\n"
  },
  {
    "path": "server/controllers/assets/utils/transformImage.js",
    "content": "const gm = require('gm');\nconst logger = require('winston');\nconst imageMagick = gm.subClass({ imageMagick: true });\nconst { getImageHeightAndWidth } = require('../../../utils/imageProcessing');\n\nmodule.exports = function transformImage(path, queryObj) {\n  return new Promise((resolve, reject) => {\n    let { h: cHeight = null } = queryObj;\n    let { w: cWidth = null } = queryObj;\n    let { t: transform = null } = queryObj;\n    let { x: xOrigin = null } = queryObj;\n    let { y: yOrigin = null } = queryObj;\n    let oHeight,\n      oWidth = null;\n    try {\n      getImageHeightAndWidth(path).then(hwarr => {\n        oHeight = hwarr[0];\n        oWidth = hwarr[1];\n        // conditional logic here\n        if (transform === 'crop') {\n          resolve(_cropCenter(path, cWidth, cHeight, oWidth, oHeight));\n        } else if (transform === 'stretch') {\n          imageMagick(path)\n            .resize(cWidth, cHeight, '!')\n            .toBuffer(null, (err, buf) => {\n              resolve(buf);\n            });\n        } else {\n          // resize scaled\n          imageMagick(path)\n            .resize(cWidth, cHeight)\n            .toBuffer(null, (err, buf) => {\n              resolve(buf);\n            });\n        }\n      });\n    } catch (e) {\n      logger.error(e);\n      reject(e);\n    }\n  });\n};\n\nfunction _cropCenter(path, cropWidth, cropHeight, originalWidth, originalHeight) {\n  let oAspect = originalWidth / originalHeight;\n  let cAspect = cropWidth / cropHeight;\n  let resizeX,\n    resizeY,\n    xpoint,\n    ypoint = null;\n\n  if (oAspect >= cAspect) {\n    // if crop is narrower aspect than original\n    resizeY = cropHeight;\n    xpoint = (oAspect * cropHeight) / 2 - cropWidth / 2;\n    ypoint = 0;\n  } else {\n    // if crop is wider aspect than original\n    resizeX = cropWidth;\n    xpoint = 0;\n    ypoint = cropWidth / oAspect / 2 - cropHeight / 2;\n  }\n  return new Promise((resolve, reject) => {\n    try {\n      imageMagick(path)\n        .resize(resizeX, resizeY)\n        .crop(cropWidth, cropHeight, xpoint, ypoint)\n        .toBuffer(null, (err, buf) => {\n          resolve(buf);\n        });\n    } catch (e) {\n      logger.error(e);\n      reject(e);\n    }\n  });\n}\n"
  },
  {
    "path": "server/controllers/auth/login/index.js",
    "content": "const speechPassport = require('../../../speechPassport/index');\n\nconst login = (req, res, next) => {\n  speechPassport.authenticate('local-login', (err, user, info) => {\n    if (err) {\n      return next(err);\n    }\n    if (!user) {\n      return res.status(400).json({\n        success: false,\n        message: info.message,\n      });\n    }\n    req.logIn(user, (err) => {\n      if (err) {\n        return next(err);\n      }\n      return res.status(200).json({\n        success       : true,\n        channelName   : req.user.channelName,\n        channelClaimId: req.user.channelClaimId,\n        shortChannelId: req.user.shortChannelId,\n      });\n    });\n  })(req, res, next);\n};\n\nmodule.exports = login;\n"
  },
  {
    "path": "server/controllers/auth/logout/index.js",
    "content": "const logout = (req, res) => {\n  req.logout();\n  const responseObject = {\n    success: true,\n    message: 'you successfully logged out',\n  };\n  res.status(200).json(responseObject);\n};\n\nmodule.exports = logout;\n"
  },
  {
    "path": "server/controllers/auth/signup/index.js",
    "content": "const signup = (req, res) => {\n  res.status(200).json({\n    success       : true,\n    channelName   : req.user.channelName,\n    channelClaimId: req.user.channelClaimId,\n    shortChannelId: req.user.shortChannelId,\n  });\n};\n\nmodule.exports = signup;\n"
  },
  {
    "path": "server/controllers/auth/user/index.js",
    "content": "const user = (req, res) => {\n  const responseObject = {\n    success: true,\n    data   : req.user,\n  };\n  res.status(200).json(responseObject);\n};\n\nmodule.exports = user;\n"
  },
  {
    "path": "server/controllers/pages/sendReactApp.js",
    "content": "import handleShowRender from '../../render/handleShowRender';\n\nexport default (req, res) => {\n  handleShowRender(req, res);\n};\n"
  },
  {
    "path": "server/controllers/pages/sendVideoEmbedPage.js",
    "content": "const {\n  assetDefaults: { thumbnail },\n  details: { host },\n} = require('@config/siteConfig');\n\nconst padSizes = {\n  small : 'padSmall',\n  medium: 'padMedium',\n  large : 'padLarge',\n};\n\nconst argumentProcessors = {\n  'bottom': async (config) => {\n    config.classNames.push('bottom');\n  },\n  'right': async (config) => {\n    config.classNames.push('right');\n  },\n  'pad': async (config, val) => {\n    config.classNames.push(padSizes[val]);\n  },\n  'logoClaim': async (config, val) => {\n    config.logoUrl = `${host}/${val}`;\n  },\n  'link': async (config, val) => {\n    config.logoLink = val;\n  },\n};\n\nconst parseLogoConfigParam = async (rawConfig) => {\n  if (rawConfig) {\n    let parsedConfig = {\n      classNames: ['logoLink'],\n      logoUrl   : thumbnail,\n    };\n    let splitConfig;\n    try {\n      splitConfig = rawConfig.split(',');\n    } catch (e) { }\n\n    if (!splitConfig) {\n      return false;\n    }\n\n    for (let i = 0; i < splitConfig.length; i++) {\n      let currentArgument = splitConfig[i];\n\n      if (argumentProcessors[currentArgument]) {\n        await argumentProcessors[currentArgument](parsedConfig);\n      } else {\n        const splitArgument = currentArgument.split(':');\n        if (argumentProcessors[splitArgument[0]]) {\n          await argumentProcessors[splitArgument[0]](parsedConfig, splitArgument[1]);\n        }\n      }\n    }\n\n    parsedConfig.classNames = parsedConfig.classNames.join(' ');\n\n    return parsedConfig;\n  }\n\n  return false;\n};\n\nconst sendVideoEmbedPage = async ({ params }, res) => {\n  let {\n    claimId,\n    config,\n    name,\n  } = params;\n\n  // if channel then swap name and claimId for order\n  if (name[0] === '@' && name.includes(':')) {\n    const temp = name;\n    name = claimId;\n    claimId = temp;\n  }\n\n  const logoConfig = await parseLogoConfigParam(config);\n\n  // test setting response headers\n  console.log('removing x-frame-options');\n  res.removeHeader('X-Frame-Options');\n  // get and render the content\n  res.status(200).render('embed', { host, claimId, name, logoConfig });\n};\n\nmodule.exports = sendVideoEmbedPage;\n"
  },
  {
    "path": "server/controllers/utils/errorHandlers.js",
    "content": "const logger = require('winston');\n\nmodule.exports = {\n  handleErrorResponse: function (originalUrl, ip, error, res) {\n    logger.error(`Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));\n    const [status, message] = module.exports.returnErrorMessageAndStatus(error);\n    res\n      .status(status)\n      .json(module.exports.createErrorResponsePayload(status, message));\n  },\n  returnErrorMessageAndStatus: function (error) {\n    let status, message;\n    // check for daemon being turned off\n    if (error.code === 'ECONNREFUSED') {\n      status = 503;\n      message = 'Connection refused.  The daemon may not be running.';\n      // fallback for everything else\n    } else {\n      status = 400;\n      if (error.message) {\n        message = error.message;\n      } else {\n        message = error;\n      }\n    }\n    return [status, message];\n  },\n  useObjectPropertiesIfNoKeys: function (err) {\n    if (Object.keys(err).length === 0) {\n      let newErrorObject = {};\n      Object.getOwnPropertyNames(err).forEach((key) => {\n        newErrorObject[key] = err[key];\n      });\n      return newErrorObject;\n    }\n    return err;\n  },\n  createErrorResponsePayload (status, message) {\n    return {\n      status,\n      success: false,\n      message,\n    };\n  },\n};\n"
  },
  {
    "path": "server/controllers/utils/getClaimId.js",
    "content": "const logger = require('winston');\n\nconst db = require('../../models');\nconst chainquery = require('chainquery').default;\n\nconst getClaimIdByChannel = async (channelName, channelClaimId, claimName) => {\n  logger.debug(`getClaimIdByChannel(${channelName}, ${channelClaimId}, ${claimName})`);\n\n  let channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelClaimId);\n\n  if (channelId === null) {\n    channelId = await db.Certificate.getLongChannelId(channelName, channelClaimId);\n  }\n\n  let claimId = await chainquery.claim.queries.getClaimIdByLongChannelId(channelId, claimName);\n\n  if (claimId === null) {\n    claimId = db.Claim.getClaimIdByLongChannelId(channelId, claimName);\n  }\n\n  return claimId;\n};\n\nconst getClaimId = async (channelName, channelClaimId, name, claimId) => {\n  logger.debug(`getClaimId: ${channelName}, ${channelClaimId}, ${name}, ${claimId})`);\n  if (channelName) {\n    return getClaimIdByChannel(channelName, channelClaimId, name);\n  } else {\n    let claimIdResult = await chainquery.claim.queries.getLongClaimId(name, claimId);\n\n    if (!claimIdResult) {\n      claimIdResult = await db.Claim.getLongClaimId(name, claimId);\n    }\n\n    return claimIdResult;\n  }\n};\n\nmodule.exports = getClaimId;\n"
  },
  {
    "path": "server/controllers/utils/redirect.js",
    "content": "const redirect = (route) => {\n  return (req, res) => {\n    res.status(301).redirect(route);\n  };\n};\n\nmodule.exports = redirect;\n"
  },
  {
    "path": "server/index.js",
    "content": "// load modules\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst expressHandlebars = require('express-handlebars');\nconst helmet = require('helmet');\nconst cookieSession = require('cookie-session');\nconst http = require('http');\nconst logger = require('winston');\nconst Path = require('path');\nconst httpContext = require('express-http-context');\n\n// load local modules\nconst db = require('./models');\nconst requestLogger = require('./middleware/requestLogger');\nconst createDatabaseIfNotExists = require('./models/utils/createDatabaseIfNotExists');\nconst { getAccountBalance } = require('./lbrynet/index');\nconst configureLogging = require('./utils/configureLogging');\nconst configureSlack = require('./utils/configureSlack');\nconst { setupBlockList } = require('./utils/blockList');\nconst speechPassport = require('./speechPassport');\nconst processTrending = require('./utils/processTrending');\n\nconst { setRouteDataInContextMiddleware } = require('./middleware/httpContextMiddleware');\n\nconst {\n  details: { port: PORT, blockListEndpoint },\n  startup: { performChecks, performUpdates },\n} = require('@config/siteConfig');\n\nconst { sessionKey } = require('@private/authConfig.json');\n\n// configure.js doesn't handle new keys in config.json files yet. Make sure it doens't break.\nlet finalBlockListEndpoint;\n\nfunction Server() {\n  this.initialize = () => {\n    // configure logging\n    configureLogging();\n    // configure slack logging\n    configureSlack();\n  };\n  this.createApp = () => {\n    /* create app */\n    const app = express();\n\n    if (process.env.NODE_ENV === 'development') {\n      const webpack = require('webpack');\n      const webpackDevMiddleware = require('webpack-dev-middleware');\n\n      const webpackClientConfig = require('../webpack/webpack.client.config')(null, {\n        mode: 'development',\n      });\n      const clientCompiler = webpack(webpackClientConfig);\n\n      app.use(\n        webpackDevMiddleware(clientCompiler, {\n          publicPath: webpackClientConfig.output.publicPath,\n        })\n      );\n\n      app.use(require('webpack-hot-middleware')(clientCompiler));\n    }\n\n    // trust the proxy to get ip address for us\n    app.enable('trust proxy');\n\n    app.use((req, res, next) => {\n      if (\n        req.get('User-Agent') ===\n        'Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1'\n      ) {\n        res\n          .status(403)\n          .send(\n            '<h1>Forbidden</h1>If you are seeing this by mistake, please contact us using <a href=\"https://chat.lbry.com/\">https://chat.lbry.com/</a>'\n          );\n        res.end();\n      } else {\n        next();\n      }\n    });\n\n    // set HTTP headers to protect against well-known web vulnerabilties\n    app.use(helmet());\n\n    // Support per-request http-context\n    app.use(httpContext.middleware);\n\n    // 'express.static' to serve static files from site/public directory\n    const sitePublicPath = Path.resolve(process.cwd(), 'site/public');\n    app.use(express.static(sitePublicPath));\n    logger.info(`serving static files from site static path at ${sitePublicPath}.`);\n\n    // 'express.static' to serve static files from public directory\n    const publicPath = Path.resolve(process.cwd(), 'public');\n    app.use(express.static(publicPath));\n    logger.info(`serving static files from default static path at ${publicPath}.`);\n\n    // 'body parser' for parsing application/json\n    app.use(bodyParser.json());\n\n    // 'body parser' for parsing application/x-www-form-urlencoded\n    app.use(bodyParser.urlencoded({ extended: true }));\n\n    // add custom middleware (note: build out to accept dynamically use what is in server/middleware/\n    app.use(requestLogger);\n\n    // initialize passport\n    app.use(\n      cookieSession({\n        name: 'session',\n        keys: [sessionKey],\n      })\n    );\n    app.use(speechPassport.initialize());\n    app.use(speechPassport.session());\n\n    // configure handlebars & register it with express app\n    const viewsPath = Path.resolve(process.cwd(), 'server/views');\n    app.engine(\n      'handlebars',\n      expressHandlebars({\n        async: false,\n        dataType: 'text',\n        defaultLayout: 'embed',\n        partialsDir: Path.join(viewsPath, '/partials'),\n        layoutsDir: Path.join(viewsPath, '/layouts'),\n      })\n    );\n    app.set('views', viewsPath);\n    app.set('view engine', 'handlebars');\n\n    // set the routes on the app\n    const routes = require('./routes');\n\n    Object.keys(routes).map(routePath => {\n      let routeData = routes[routePath];\n      let routeMethod = routeData.hasOwnProperty('method') ? routeData.method : 'get';\n      let controllers = Array.isArray(routeData.controller)\n        ? routeData.controller\n        : [routeData.controller];\n\n      app[routeMethod](\n        routePath,\n        // logMetricsMiddleware,\n        setRouteDataInContextMiddleware(routePath, routeData),\n        ...controllers\n      );\n    });\n\n    this.app = app;\n  };\n  this.createServer = () => {\n    /* create server */\n    this.server = http.Server(this.app);\n  };\n  this.startServerListening = () => {\n    logger.info(`Starting server on ${PORT}...`);\n    return new Promise((resolve, reject) => {\n      this.server.listen(PORT, () => {\n        logger.info(`Server is listening on PORT ${PORT}`);\n        resolve();\n      });\n    });\n  };\n  this.syncDatabase = () => {\n    logger.info(`Syncing database...`);\n    return createDatabaseIfNotExists().then(() => {\n      db.sequelize.sync();\n    });\n  };\n  this.performChecks = () => {\n    if (!performChecks) {\n      return;\n    }\n    logger.info(`Performing checks...`);\n    return Promise.all([getAccountBalance()]).then(([accountBalance]) => {\n      logger.info('Starting LBC balance:', accountBalance);\n    });\n  };\n\n  this.performUpdates = () => {\n    if (!performUpdates) {\n      return;\n    }\n    if (blockListEndpoint) {\n      finalBlockListEndpoint = blockListEndpoint;\n    } else if (!blockListEndpoint) {\n      if (typeof blockListEndpoint !== 'string') {\n        logger.warn(\n          'blockListEndpoint is null due to outdated siteConfig file. \\n' +\n            'Continuing with default LBRY blocklist api endpoint. \\n ' +\n            '(Specify /\"blockListEndpoint\" : \"\"/ to disable.'\n        );\n        finalBlockListEndpoint = 'https://api.lbry.com/file/list_blocked';\n      }\n    }\n    logger.info(`Peforming updates...`);\n    if (!finalBlockListEndpoint) {\n      logger.info('Configured for no Block List');\n      db.Tor.refreshTable().then(updatedTorList => {\n        logger.info('Tor list updated, length:', updatedTorList.length);\n      });\n    } else {\n      return Promise.all([\n        db.Blocked.refreshTable(finalBlockListEndpoint),\n        db.Tor.refreshTable(),\n      ]).then(([updatedBlockedList, updatedTorList]) => {\n        logger.info('Blocked list updated, length:', updatedBlockedList.length);\n        logger.info('Tor list updated, length:', updatedTorList.length);\n      });\n    }\n  };\n  this.start = () => {\n    this.initialize();\n    this.createApp();\n    this.createServer();\n    this.syncDatabase()\n      .then(() => {\n        return this.startServerListening();\n      })\n      .then(() => {\n        return Promise.all([this.performChecks(), this.performUpdates()]);\n      })\n      .then(() => {\n        return setupBlockList();\n      })\n      .then(() => {\n        logger.info('Spee.ch startup is complete');\n\n        setInterval(processTrending, 30 * 60000); // 30 minutes\n      })\n      .catch(error => {\n        if (error.code === 'ECONNREFUSED') {\n          return logger.error('Connection refused.  The daemon may not be running.');\n        } else if (error.code === 'EADDRINUSE') {\n          return logger.error('Server could not start listening.  The port is already in use.');\n        } else if (error.message) {\n          logger.error(error.message);\n        }\n        logger.error(error);\n      });\n  };\n}\n\nmodule.exports = Server;\n"
  },
  {
    "path": "server/lbrynet/index.js",
    "content": "const axios = require('axios');\nconst logger = require('winston');\nconst { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');\nconst lbrynetUri = 'http://' + apiHost + ':' + apiPort;\nconst db = require('../models');\nconst { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');\nconst handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');\nconst { publishing } = require('@config/siteConfig');\n\nmodule.exports = {\n  publishClaim(publishParams) {\n    logger.debug(`lbryApi >> Publishing claim to \"${publishParams.name}\"`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'publish',\n          params: publishParams,\n        })\n        .then(response => {\n          sendGATimingEvent(\n            'lbrynet',\n            'publish',\n            chooseGaLbrynetPublishLabel(publishParams),\n            gaStartTime,\n            Date.now()\n          );\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  getClaim(uri) {\n    logger.debug(`lbryApi >> Getting Claim for \"${uri}\"`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'get',\n          params: {\n            uri,\n            timeout: getTimeout || 30,\n          },\n        })\n        .then(response => {\n          sendGATimingEvent('lbrynet', 'getClaim', 'GET', gaStartTime, Date.now());\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  getFileListFileByOutpoint(outpoint) {\n    logger.debug(`lbryApi >> Getting File_List for \"${outpoint}\"`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'file_list',\n          params: {\n            outpoint,\n          },\n        })\n        .then(response => {\n          sendGATimingEvent('lbrynet', 'getFileList', 'FILE_LIST', gaStartTime, Date.now());\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  async abandonClaim({ outpoint }) {\n    logger.debug(`lbryApi >> Abandon claim \"${outpoint}\"`);\n    const gaStartTime = Date.now();\n    const [txid, nout] = outpoint.split(':');\n    try {\n      const abandon = await axios.post(lbrynetUri, {\n        method: 'stream_abandon',\n        params: { txid: txid, nout: Number(nout) },\n      });\n      sendGATimingEvent('lbrynet', 'abandonClaim', 'ABANDON_CLAIM', gaStartTime, Date.now());\n      return abandon.data;\n    } catch (error) {\n      logger.error(error);\n      return error;\n    }\n  },\n  getClaimList(claimName) {\n    logger.debug(`lbryApi >> Getting claim_list for \"${claimName}\"`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'claim_list',\n          params: { name: claimName },\n        })\n        .then(response => {\n          sendGATimingEvent('lbrynet', 'getClaimList', 'CLAIM_LIST', gaStartTime, Date.now());\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  resolveUri(uri) {\n    logger.debug(`lbryApi >> Resolving URI for \"${uri}\"`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'resolve',\n          params: { urls: uri },\n        })\n        .then(({ data }) => {\n          sendGATimingEvent('lbrynet', 'resolveUri', 'RESOLVE', gaStartTime, Date.now());\n          if (Object.keys(data.result).length === 0 && data.result.constructor === Object) {\n            // workaround for daemon returning empty result object\n            // https://github.com/lbryio/lbry/issues/1485\n            db.Claim.findOne({ where: { claimId: uri.split('#')[1] } })\n              .then(() => reject('This claim has not yet been confirmed on the LBRY blockchain'))\n              .catch(() => reject(`Claim ${uri} does not exist`));\n          } else if (data.result[uri].error) {\n            // check for errors\n            reject(data.result[uri].error);\n          } else {\n            // if no errors, resolve\n            resolve(data.result[uri]);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  getDownloadDirectory() {\n    logger.debug('lbryApi >> Retrieving the download directory path from lbry daemon...');\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'settings_get',\n        })\n        .then(({ data }) => {\n          sendGATimingEvent(\n            'lbrynet',\n            'getDownloadDirectory',\n            'SETTINGS_GET',\n            gaStartTime,\n            Date.now()\n          );\n          if (data.result) {\n            resolve(data.result.download_dir);\n          } else {\n            return new Error(\n              'Successfully connected to lbry daemon, but unable to retrieve the download directory.'\n            );\n          }\n        })\n        .catch(error => {\n          logger.error('Lbrynet Error:', error);\n          resolve('/home/lbry/Downloads/');\n        });\n    });\n  },\n  createChannel(name) {\n    logger.debug(`lbryApi >> Creating channel for ${name}...`);\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'channel_create',\n          params: {\n            name: name,\n            bid: publishing.channelClaimBidAmount,\n          },\n        })\n        .then(response => {\n          sendGATimingEvent('lbrynet', 'createChannel', 'CHANNEL_NEW', gaStartTime, Date.now());\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n  getAccountBalance() {\n    const gaStartTime = Date.now();\n    return new Promise((resolve, reject) => {\n      axios\n        .post(lbrynetUri, {\n          method: 'account_balance',\n        })\n        .then(response => {\n          sendGATimingEvent(\n            'lbrynet',\n            'getAccountBalance',\n            'SETTINGS_GET',\n            gaStartTime,\n            Date.now()\n          );\n          handleLbrynetResponse(response, resolve, reject);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  },\n};\n"
  },
  {
    "path": "server/lbrynet/utils/handleLbrynetResponse.js",
    "content": "const logger = require('winston');\n\nconst handleLbrynetResponse = ({ data }, resolve, reject) => {\n  logger.debug('lbry api data:', data);\n  if (data) {\n    // check for an error\n    if (data.error) {\n      logger.debug('Lbrynet api error:', data.error);\n      reject(new Error(data.error.message));\n      return;\n    }\n    resolve(data.result);\n    return;\n  }\n  // fallback in case it just timed out\n  reject(JSON.stringify(data));\n};\n\nmodule.exports = handleLbrynetResponse;\n"
  },
  {
    "path": "server/middleware/autoblockPublishMiddleware.js",
    "content": "const fs = require('fs');\n\nconst logger = require('winston');\nconst {\n  publishing: { publishingChannelWhitelist },\n} = require('@config/siteConfig');\nconst ipBanFile = './site/config/ipBan.txt';\nconst ipWhitelist = './site/config/ipWhitelist.txt';\nconst forbiddenMessage =\n  '<h1>Forbidden</h1>If you are seeing this by mistake, please contact us using <a href=\"https://chat.lbry.com/\">https://chat.lbry.com/</a>';\nconst maxPublishesInTenMinutes = 20;\nlet ipCounts = {};\nlet blockedAddresses = [];\nlet whitelistedAddresses = [];\n\nif (fs.existsSync(ipBanFile)) {\n  const lineReader = require('readline').createInterface({\n    input: require('fs').createReadStream(ipBanFile),\n  });\n\n  lineReader.on('line', line => {\n    if (line && line !== '') {\n      blockedAddresses.push(line);\n    }\n  });\n}\n\n// If a file called ipWhitelist.txt exists\n// Please comment above each whitelisted IP why/who/when etc\n// # Jim because he's awesome - January 2018\nif (fs.existsSync(ipWhitelist)) {\n  const lineReader = require('readline').createInterface({\n    input: require('fs').createReadStream(ipWhitelist),\n  });\n\n  lineReader.on('line', line => {\n    if (line && line !== '' && line[0] !== '#') {\n      whitelistedAddresses.push(line);\n    }\n  });\n}\n\nconst autoblockPublishMiddleware = (req, res, next) => {\n  let ip = (req.headers['x-forwarded-for'] || req.connection.remoteAddress).split(/,\\s?/)[0];\n\n  if (whitelistedAddresses.indexOf(ip) !== -1) {\n    next();\n    return;\n  }\n  if (blockedAddresses.indexOf(ip) !== -1) {\n    res.status(403).send(forbiddenMessage);\n    res.end();\n\n    return;\n  }\n\n  let count = (ipCounts[ip] = (ipCounts[ip] || 0) + 1);\n\n  setTimeout(() => {\n    if (ipCounts[ip]) {\n      ipCounts[ip]--;\n      if (ipCounts[ip] === 0) {\n        delete ipCounts[ip];\n      }\n    }\n  }, 600000 /* 10 minute retainer */);\n\n  if (count === maxPublishesInTenMinutes) {\n    logger.error(`Banning IP: ${ip}`);\n    blockedAddresses.push(ip);\n    res.status(403).send(forbiddenMessage);\n    res.end();\n\n    fs.appendFile(ipBanFile, ip + '\\n', () => {});\n  } else {\n    next();\n  }\n};\n\nconst autoblockPublishBodyMiddleware = (req, res, next) => {\n  if (req.body && publishingChannelWhitelist) {\n    let ip = (req.headers['x-forwarded-for'] || req.connection.remoteAddress).split(/,\\s?/)[0];\n    const { channelName } = req.body;\n\n    if (channelName && publishingChannelWhitelist.indexOf(channelName.toLowerCase()) !== -1) {\n      delete ipCounts[ip];\n    }\n  }\n  next();\n};\n\nmodule.exports = {\n  autoblockPublishMiddleware,\n  autoblockPublishBodyMiddleware,\n};\n"
  },
  {
    "path": "server/middleware/httpContextMiddleware.js",
    "content": "const httpContext = require('express-http-context');\n\nfunction setRouteDataInContextMiddleware(routePath, routeData) {\n  return function(req, res, next) {\n    httpContext.set('routePath', routePath);\n    httpContext.set('routeData', routeData);\n    next();\n  };\n}\n\nmodule.exports = {\n  setRouteDataInContextMiddleware,\n};\n"
  },
  {
    "path": "server/middleware/logMetricsMiddleware.js",
    "content": "const logger = require('winston');\nconst db = require('../models');\nconst httpContext = require('express-http-context');\n\nfunction logMetricsMiddleware (req, res, next) {\n  res.on('finish', () => {\n    const userAgent = req.get('user-agent');\n    const routePath = httpContext.get('routePath');\n\n    let referrer = req.get('referrer');\n\n    if (referrer && referrer.length > 255) {\n      try {\n        // Attempt to \"safely\" clamp long URLs\n        referrer = /(.*?)#.*/.exec(referrer)[1];\n      } catch (e) {\n        // Cheap forced string conversion & clamp\n        referrer = String(referrer);\n        referrer = referrer.substr(0, 255);\n      }\n\n      if (referrer.length > 255) {\n        logger.warn('Request refferer exceeds 255 characters:', referrer);\n        referrer = referrer.substring(0, 255);\n      }\n    }\n\n    db.Metrics.create({\n      time      : Date.now(),\n      isInternal: /node-fetch/.test(userAgent),\n      isChannel : res.isChannel,\n      claimId   : res.claimId,\n      routePath : httpContext.get('routePath'),\n      params    : JSON.stringify(req.params),\n      ip        : req.headers['x-forwarded-for'] || req.connection.remoteAddress,\n      request   : req.url,\n      routeData : JSON.stringify(httpContext.get('routeData')),\n      referrer,\n      userAgent,\n    });\n  });\n\n  next();\n}\n\nfunction setRouteDataInContextMiddleware (routePath, routeData) {\n  return function (req, res, next) {\n    httpContext.set('routePath', routePath);\n    httpContext.set('routeData', routeData);\n    next();\n  };\n}\n\nmodule.exports = {\n  logMetricsMiddleware,\n  setRouteDataInContextMiddleware,\n};\n"
  },
  {
    "path": "server/middleware/multipartMiddleware.js",
    "content": "const multipart = require('connect-multiparty');\nconst { publishing: { uploadDirectory } } = require('@config/siteConfig');\nconst multipartMiddleware = multipart({uploadDir: uploadDirectory});\n\nmodule.exports = multipartMiddleware;\n"
  },
  {
    "path": "server/middleware/requestLogger.js",
    "content": "const logger = require('winston');\n\nconst requestLogger = (req, res, next) => {  // custom logging middleware to log all incoming http requests\n  logger.debug(`Request on ${req.originalUrl} from ${req.ip}`);\n  next();\n};\n\nmodule.exports = requestLogger;\n"
  },
  {
    "path": "server/middleware/torCheckMiddleware.js",
    "content": "const logger = require('winston');\nconst db = require('../models');\n\nconst torCheck = (req, res, next) => {\n  const { ip } = req;\n  logger.debug(`tor check for: ${ip}`);\n  return db.Tor.findAll(\n    {\n      where: {\n        address: ip,\n      },\n      raw: true,\n    })\n    .then(result => {\n      if (result.length >= 1) {\n        logger.info('Tor request blocked:', ip);\n        const failureResponse = {\n          success: false,\n          message: 'Unfortunately this api route is not currently available for tor users.  We are working on a solution that will allow tor users to use this endpoint in the future.',\n        };\n        res.status(403).json(failureResponse);\n      } else {\n        return next();\n      }\n    })\n    .catch(error => {\n      logger.error(error);\n    });\n};\n\nmodule.exports = torCheck;\n"
  },
  {
    "path": "server/migrations/ChangeCertificateColumnTypes2.js",
    "content": "module.exports = {\n  up: (queryInterface, Sequelize) => {\n    // logic for transforming into the new state\n    const p1 = queryInterface.changeColumn(\n      'Certificate',\n      'amount',\n      {\n        type     : Sequelize.DECIMAL(19, 8),\n        allowNull: true,\n      }\n    );\n    const p2 = queryInterface.changeColumn(\n      'Certificate',\n      'effectiveAmount',\n      {\n        type     : Sequelize.DECIMAL(19, 8),\n        allowNull: true,\n      }\n    );\n    return Promise.all([p1, p2]);\n  },\n  down: (queryInterface, Sequelize) => {\n    // logic for reverting the changes\n    const p1 = queryInterface.changeColumn(\n      'Certificate',\n      'amount',\n      {\n        type     : Sequelize.DOUBLE,\n        allowNull: true,\n      }\n    );\n    const p2 = queryInterface.changeColumn(\n      'Certificate',\n      'effectiveAmount',\n      {\n        type     : Sequelize.DOUBLE,\n        allowNull: true,\n      }\n    );\n    return Promise.all([p1, p2]);\n  },\n};\n"
  },
  {
    "path": "server/migrations/ChangeClaimColumnTypes.js",
    "content": "module.exports = {\n  up: (queryInterface, Sequelize) => {\n    // logic for transforming into the new state\n    const p1 = queryInterface.changeColumn(\n      'Claim',\n      'amount',\n      {\n        type     : Sequelize.DECIMAL(19, 8),\n        allowNull: true,\n      }\n    );\n    const p2 = queryInterface.changeColumn(\n      'Claim',\n      'effectiveAmount',\n      {\n        type     : Sequelize.DECIMAL(19, 8),\n        allowNull: true,\n      }\n    );\n    return Promise.all([p1, p2]);\n  },\n  down: (queryInterface, Sequelize) => {\n    // logic for reverting the changes\n    const p1 = queryInterface.changeColumn(\n      'Claim',\n      'amount',\n      {\n        type     : Sequelize.DOUBLE,\n        allowNull: true,\n      }\n    );\n    const p2 = queryInterface.changeColumn(\n      'Claim',\n      'effectiveAmount',\n      {\n        type     : Sequelize.DOUBLE,\n        allowNull: true,\n      }\n    );\n    return Promise.all([p1, p2]);\n  },\n};\n"
  },
  {
    "path": "server/migrations/File_AddHeightAndWidthColumn.js",
    "content": "module.exports = {\n  up: (queryInterface, { INTEGER }) => {\n    // logic for transforming into the new state\n    return Promise.all([\n      queryInterface.addColumn(\n        'File',\n        'fileHeight',\n        {\n          type     : INTEGER,\n          allowNull: false,\n          default  : 0,\n        }\n      ),\n      queryInterface.addColumn(\n        'File',\n        'fileWidth',\n        {\n          type     : INTEGER,\n          allowNull: false,\n          default  : 0,\n        }\n      ),\n      queryInterface.removeColumn(\n        'File',\n        'address',\n      ),\n      queryInterface.removeColumn(\n        'File',\n        'height',\n      ),\n      queryInterface.removeColumn(\n        'File',\n        'nsfw',\n      ),\n      queryInterface.removeColumn(\n        'File',\n        'trendingEligible',\n      ),\n    ]);\n  },\n  down: (queryInterface, { BOOLEAN, INTEGER, STRING }) => {\n    return Promise.all([\n      queryInterface.removeColumn(\n        'File',\n        'fileHeight',\n      ),\n      queryInterface.removeColumn(\n        'File',\n        'fileWidth',\n      ),\n      queryInterface.addColumn(\n        'File',\n        'address',\n        {\n          type     : STRING,\n          allowNull: false,\n        }\n      ),\n      queryInterface.addColumn(\n        'File',\n        'height',\n        {\n          type     : INTEGER,\n          allowNull: false,\n          default  : 0,\n        }\n      ),\n      queryInterface.addColumn(\n        'File',\n        'nsfw',\n        {\n          type        : BOOLEAN,\n          allowNull   : false,\n          defaultValue: false,\n        }\n      ),\n      queryInterface.addColumn(\n        'File',\n        'trendingEligible',\n        {\n          type        : BOOLEAN,\n          allowNull   : false,\n          defaultValue: true,\n        }\n      ),\n    ]);\n  },\n};\n"
  },
  {
    "path": "server/models/blocked.js",
    "content": "const logger = require('winston');\nconst BLOCKED_CLAIM = 'BLOCKED_CLAIM';\n\nmodule.exports = (sequelize, { STRING }) => {\n  const Blocked = sequelize.define(\n    'Blocked',\n    {\n      outpoint: {\n        type     : STRING,\n        allowNull: false,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  Blocked.getBlockList = function () {\n    logger.debug('returning full block list');\n    return new Promise((resolve, reject) => {\n      this.findAll()\n        .then(list => { return resolve(list) });\n    });\n  };\n\n  Blocked.isNotBlocked = function (outpoint) {\n    logger.debug(`checking to see if ${outpoint} is not blocked`);\n    return new Promise((resolve, reject) => {\n      this.findOne({\n        where: {\n          outpoint,\n        },\n      })\n        .then(result => {\n          if (result) {\n            return reject(BLOCKED_CLAIM);\n          }\n          resolve(true);\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(BLOCKED_CLAIM);\n        });\n    });\n  };\n\n  Blocked.refreshTable = function (blockEndpoint) {\n    let blockedList = [];\n\n    return fetch(blockEndpoint)\n      .then(response => {\n        return response.json();\n      })\n      .then(jsonResponse => {\n        if (!jsonResponse.data) {\n          throw new Error('no data in list_blocked response');\n        }\n        if (!jsonResponse.data.outpoints) {\n          throw new Error('no outpoints in list_blocked response');\n        }\n        let outpoints = jsonResponse.data.outpoints;\n        logger.debug('total outpoints:', outpoints.length);\n        // prep the records\n        for (let i = 0; i < outpoints.length; i++) {\n          blockedList.push({\n            outpoint: outpoints[i],\n          });\n        }\n        // clear the table\n        return this.destroy({\n          truncate: true,\n        });\n      })\n      .then(() => {\n        // fill the table\n        return this.bulkCreate(blockedList);\n      });\n  };\n\n  return Blocked;\n};\n"
  },
  {
    "path": "server/models/certificate.js",
    "content": "const logger = require('winston');\nconst returnShortId = require('./utils/returnShortId.js');\n\nconst NO_CHANNEL = 'NO_CHANNEL';\n\nfunction isLongChannelId (channelId) {\n  return (channelId && (channelId.length === 40));\n}\n\nfunction isShortChannelId (channelId) {\n  return (channelId && (channelId.length < 40));\n}\n\nmodule.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {\n  const Certificate = sequelize.define(\n    'Certificate',\n    {\n      address: {\n        type   : STRING,\n        default: null,\n      },\n      amount: {\n        type   : DECIMAL(19, 8),\n        default: null,\n      },\n      claimId: {\n        type   : STRING,\n        default: null,\n      },\n      claimSequence: {\n        type   : INTEGER,\n        default: null,\n      },\n      decodedClaim: {\n        type   : BOOLEAN,\n        default: null,\n      },\n      depth: {\n        type   : INTEGER,\n        default: null,\n      },\n      effectiveAmount: {\n        type   : DECIMAL(19, 8),\n        default: null,\n      },\n      hasSignature: {\n        type   : BOOLEAN,\n        default: null,\n      },\n      height: {\n        type   : INTEGER,\n        default: null,\n      },\n      hex: {\n        type   : TEXT('long'),\n        default: null,\n      },\n      name: {\n        type   : STRING,\n        default: null,\n      },\n      nout: {\n        type   : INTEGER,\n        default: null,\n      },\n      txid: {\n        type   : STRING,\n        default: null,\n      },\n      validAtHeight: {\n        type   : INTEGER,\n        default: null,\n      },\n      outpoint: {\n        type   : STRING,\n        default: null,\n      },\n      valueVersion: {\n        type   : STRING,\n        default: null,\n      },\n      claimType: {\n        type   : STRING,\n        default: null,\n      },\n      certificateVersion: {\n        type   : STRING,\n        default: null,\n      },\n      keyType: {\n        type   : STRING,\n        default: null,\n      },\n      publicKey: {\n        type   : TEXT('long'),\n        default: null,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  Certificate.associate = db => {\n    Certificate.belongsTo(db.Channel, {\n      foreignKey: {\n        allowNull: true,\n      },\n    });\n  };\n\n  Certificate.getShortChannelIdFromLongChannelId = function (longChannelId, channelName) {\n    logger.debug(`getShortChannelIdFromLongChannelId ${channelName}:${longChannelId}`);\n    return new Promise((resolve, reject) => {\n      this\n        .findAll({\n          where: {name: channelName},\n          order: [['height', 'ASC']],\n        })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CHANNEL);\n            default:\n              return resolve(returnShortId(result, longChannelId));\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Certificate.validateLongChannelId = function (name, claimId) {\n    logger.debug(`validateLongChannelId(${name}, ${claimId})`);\n    return new Promise((resolve, reject) => {\n      this.findOne({\n        where: {name, claimId},\n      })\n        .then(result => {\n          if (!result) {\n            return reject(NO_CHANNEL);\n          }\n          resolve(claimId);\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CHANNEL);\n        });\n    });\n  };\n\n  Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelClaimId) {\n    logger.debug(`getLongChannelIdFromShortChannelId(${channelName}, ${channelClaimId})`);\n    return new Promise((resolve, reject) => {\n      this\n        .findAll({\n          where: {\n            name   : channelName,\n            claimId: {\n              [sequelize.Op.like]: `${channelClaimId}%`,\n            },\n          },\n          order: [['height', 'ASC']],\n        })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CHANNEL);\n            default:\n              return resolve(result[0].claimId);\n          }\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CHANNEL);\n        });\n    });\n  };\n\n  Certificate.getLongChannelIdFromChannelName = function (channelName) {\n    logger.debug(`getLongChannelIdFromChannelName(${channelName})`);\n    return new Promise((resolve, reject) => {\n      this\n        .findAll({\n          where: { name: channelName },\n          order: [['effectiveAmount', 'DESC'], ['height', 'ASC']],\n        })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CHANNEL);\n            default:\n              return resolve(result[0].claimId);\n          }\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CHANNEL);\n        });\n    });\n  };\n\n  Certificate.getLongChannelId = function (channelName, channelClaimId) {\n    logger.debug(`getLongChannelId(${channelName}, ${channelClaimId})`);\n    if (isLongChannelId(channelClaimId)) {\n      return this.validateLongChannelId(channelName, channelClaimId);\n    } else if (isShortChannelId(channelClaimId)) {\n      return this.getLongChannelIdFromShortChannelId(channelName, channelClaimId);\n    } else {\n      return this.getLongChannelIdFromChannelName(channelName);\n    }\n  };\n\n  return Certificate;\n};\n"
  },
  {
    "path": "server/models/channel.js",
    "content": "module.exports = (sequelize, { STRING }) => {\n  const Channel = sequelize.define(\n    'Channel',\n    {\n      channelName: {\n        type     : STRING,\n        allowNull: false,\n      },\n      channelClaimId: {\n        type     : STRING,\n        allowNull: false,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  Channel.associate = db => {\n    Channel.belongsTo(db.User);\n    Channel.hasOne(db.Certificate);\n  };\n\n  return Channel;\n};\n"
  },
  {
    "path": "server/models/claim.js",
    "content": "const logger = require('winston');\nconst returnShortId = require('./utils/returnShortId.js');\nconst isApprovedChannel = require('../../utils/isApprovedChannel');\nconst {\n  assetDefaults: { thumbnail: defaultThumbnail },\n  details: { host },\n} = require('@config/siteConfig');\nconst {\n  publishing: { serveOnlyApproved, approvedChannels },\n} = require('@config/siteConfig');\n\nconst NO_CLAIM = 'NO_CLAIM';\n\nfunction determineFileExtensionFromContentType(contentType) {\n  switch (contentType) {\n    case 'image/jpeg':\n    case 'image/jpg':\n      return 'jpg';\n    case 'image/png':\n      return 'png';\n    case 'image/gif':\n      return 'gif';\n    case 'video/mp4':\n      return 'mp4';\n    case 'image/svg+xml':\n      return 'svg';\n    default:\n      logger.debug('setting unknown file type as file extension jpg');\n      return 'jpg';\n  }\n}\n\nfunction determineThumbnail(storedThumbnail, defaultThumbnail) {\n  if (storedThumbnail === '') {\n    return defaultThumbnail;\n  }\n  return storedThumbnail;\n}\n\nfunction prepareClaimData(claim) {\n  // logger.debug('preparing claim data based on resolved data:', claim);\n  claim['thumbnail'] = determineThumbnail(claim.thumbnail, defaultThumbnail);\n  claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);\n  claim['host'] = host;\n  return claim;\n}\n\nfunction isLongClaimId(claimId) {\n  return claimId && claimId.length === 40;\n}\n\nfunction isShortClaimId(claimId) {\n  return claimId && claimId.length < 40;\n}\n\nmodule.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {\n  const Claim = sequelize.define(\n    'Claim',\n    {\n      address: {\n        type: STRING,\n        default: null,\n      },\n      amount: {\n        type: DECIMAL(19, 8),\n        default: null,\n      },\n      claimId: {\n        type: STRING,\n        default: null,\n      },\n      claimSequence: {\n        type: INTEGER,\n        default: null,\n      },\n      decodedClaim: {\n        type: BOOLEAN,\n        default: null,\n      },\n      depth: {\n        type: INTEGER,\n        default: null,\n      },\n      effectiveAmount: {\n        type: DECIMAL(19, 8),\n        default: null,\n      },\n      hasSignature: {\n        type: BOOLEAN,\n        default: null,\n      },\n      height: {\n        type: INTEGER,\n        default: null,\n      },\n      hex: {\n        type: TEXT('long'),\n        default: null,\n      },\n      name: {\n        type: STRING,\n        default: null,\n      },\n      nout: {\n        type: INTEGER,\n        default: null,\n      },\n      txid: {\n        type: STRING,\n        default: null,\n      },\n      validAtHeight: {\n        type: INTEGER,\n        default: null,\n      },\n      outpoint: {\n        type: STRING,\n        default: null,\n      },\n      claimType: {\n        type: STRING,\n        default: null,\n      },\n      certificateId: {\n        type: STRING,\n        default: null,\n      },\n      author: {\n        type: STRING,\n        default: null,\n      },\n      description: {\n        type: TEXT('long'),\n        default: null,\n      },\n      language: {\n        type: STRING,\n        default: null,\n      },\n      license: {\n        type: STRING,\n        default: null,\n      },\n      licenseUrl: {\n        type: STRING,\n        default: null,\n      },\n      nsfw: {\n        type: BOOLEAN,\n        default: null,\n      },\n      preview: {\n        type: STRING,\n        default: null,\n      },\n      thumbnail: {\n        type: STRING,\n        default: null,\n      },\n      title: {\n        type: STRING,\n        default: null,\n      },\n      metadataVersion: {\n        type: STRING,\n        default: null,\n      },\n      contentType: {\n        type: STRING,\n        default: null,\n      },\n      source: {\n        type: STRING,\n        default: null,\n      },\n      sourceType: {\n        type: STRING,\n        default: null,\n      },\n      sourceVersion: {\n        type: STRING,\n        default: null,\n      },\n      streamVersion: {\n        type: STRING,\n        default: null,\n      },\n      valueVersion: {\n        type: STRING,\n        default: null,\n      },\n      channelName: {\n        type: STRING,\n        allowNull: true,\n        default: null,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  Claim.associate = db => {\n    Claim.belongsTo(db.File, {\n      foreignKey: {\n        allowNull: true,\n      },\n    });\n  };\n\n  Claim.getShortClaimIdFromLongClaimId = function(claimId, claimName) {\n    logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: { name: claimName },\n        order: [['height', 'ASC']],\n      })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              throw new Error('No claim(s) found with that claim name');\n            default:\n              resolve(returnShortId(result, claimId));\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Claim.getAllChannelClaims = function(channelClaimId) {\n    logger.debug(`Claim.getAllChannelClaims for ${channelClaimId}`);\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: { certificateId: channelClaimId },\n        order: [['height', 'DESC']],\n        raw: true, // returns an array of only data, not an array of instances\n      })\n        .then(channelClaimsArray => {\n          switch (channelClaimsArray.length) {\n            case 0:\n              return resolve(null);\n            default:\n              channelClaimsArray.forEach(claim => {\n                claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);\n                claim['thumbnail'] = determineThumbnail(claim.thumbnail, defaultThumbnail);\n                return claim;\n              });\n              return resolve(channelClaimsArray);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Claim.getClaimIdByLongChannelId = function(channelClaimId, claimName) {\n    logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`);\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: { name: claimName, certificateId: channelClaimId },\n        order: [['id', 'ASC']],\n      })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CLAIM);\n            case 1:\n              return resolve(result[0].claimId);\n            default:\n              logger.warn(\n                `${result.length} records found for \"${claimName}\" in channel \"${channelClaimId}\"`\n              );\n              return resolve(result[0].claimId);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Claim.validateLongClaimId = function(name, claimId) {\n    return new Promise((resolve, reject) => {\n      this.findOne({\n        where: {\n          name,\n          claimId,\n        },\n      })\n        .then(result => {\n          if (!result) {\n            return reject(NO_CLAIM);\n          }\n          resolve(claimId);\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CLAIM);\n        });\n    });\n  };\n\n  Claim.getLongClaimIdFromShortClaimId = function(name, shortId) {\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: {\n          name,\n          claimId: {\n            [sequelize.Op.like]: `${shortId}%`,\n          },\n        },\n        order: [['height', 'ASC']],\n      })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CLAIM);\n            default:\n              return resolve(result[0].claimId);\n          }\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CLAIM);\n        });\n    });\n  };\n\n  Claim.getTopFreeClaimIdByClaimName = function(name) {\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: { name },\n        order: [['effectiveAmount', 'DESC'], ['height', 'ASC']],\n      })\n        .then(result => {\n          switch (result.length) {\n            case 0:\n              return reject(NO_CLAIM);\n            default:\n              return resolve(result[0].dataValues.claimId);\n          }\n        })\n        .catch(error => {\n          logger.error(error);\n          reject(NO_CLAIM);\n        });\n    });\n  };\n\n  Claim.getLongClaimId = function(claimName, claimId) {\n    logger.debug(`getLongClaimId(${claimName}, ${claimId})`);\n    if (isLongClaimId(claimId)) {\n      return this.validateLongClaimId(claimName, claimId);\n    } else if (isShortClaimId(claimId)) {\n      return this.getLongClaimIdFromShortClaimId(claimName, claimId);\n    } else {\n      return this.getTopFreeClaimIdByClaimName(claimName);\n    }\n  };\n\n  Claim.fetchClaim = function(name, claimId) {\n    logger.debug(`Claim.resolveClaim: ${name} ${claimId}`);\n    return new Promise((resolve, reject) => {\n      this.findAll({\n        where: { name, claimId },\n      })\n        .then(claimArray => {\n          switch (claimArray.length) {\n            case 0:\n              return resolve(null);\n            case 1:\n              return resolve(prepareClaimData(claimArray[0].dataValues));\n            default:\n              logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);\n              return resolve(prepareClaimData(claimArray[0].dataValues));\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Claim.resolveClaim = function(name, claimId) {\n    return new Promise((resolve, reject) => {\n      this.fetchClaim(name, claimId)\n        .then(claim => {\n          logger.debug(\n            `resolveClaim: ${name}, ${claimId}, -> certificateId: ${claim && claim.certificateId}`\n          );\n          if (\n            serveOnlyApproved &&\n            !isApprovedChannel({ longId: claim.certificateId }, approvedChannels)\n          ) {\n            throw new Error('This content is unavailable');\n          }\n          return resolve(claim);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  };\n\n  Claim.getOutpoint = function(name, claimId) {\n    logger.debug(`finding outpoint for ${name}#${claimId}`);\n    return this.findAll({\n      where: { name, claimId },\n      attributes: ['outpoint'],\n    })\n      .then(result => {\n        logger.debug('outpoint result');\n        switch (result.length) {\n          case 0:\n            throw new Error(`no record found for ${name}#${claimId}`);\n          case 1:\n            return result[0].dataValues.outpoint;\n          default:\n            logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);\n            return result[0].dataValues.outpoint;\n        }\n      })\n      .catch(error => {\n        throw error;\n      });\n  };\n\n  Claim.getCurrentHeight = function() {\n    return new Promise((resolve, reject) => {\n      return this.max('height')\n        .then(result => {\n          if (result) {\n            return resolve(result);\n          }\n          return resolve(100000);\n        })\n        .catch(error => {\n          return reject(error);\n        });\n    });\n  };\n\n  return Claim;\n};\n"
  },
  {
    "path": "server/models/file.js",
    "content": "module.exports = (sequelize, { STRING, BOOLEAN, INTEGER }) => {\n  const File = sequelize.define(\n    'File',\n    {\n      name: {\n        type     : STRING,\n        allowNull: false,\n      },\n      claimId: {\n        type     : STRING,\n        allowNull: false,\n      },\n      outpoint: {\n        type     : STRING,\n        allowNull: false,\n      },\n      fileHeight: {\n        type     : INTEGER,\n        allowNull: false,\n        default  : 0,\n      },\n      fileWidth: {\n        type     : INTEGER,\n        allowNull: false,\n        default  : 0,\n      },\n      fileName: {\n        type     : STRING,\n        allowNull: false,\n      },\n      filePath: {\n        type     : STRING,\n        allowNull: false,\n      },\n      fileType: {\n        type: STRING,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  File.associate = db => {\n    File.hasOne(db.Claim);\n  };\n\n  return File;\n};\n"
  },
  {
    "path": "server/models/index.js",
    "content": "const Sequelize = require('sequelize');\nconst logger = require('winston');\n\nconst Blocked = require('./blocked');\nconst Certificate = require('./certificate');\nconst Channel = require('./channel');\nconst Claim = require('./claim');\nconst File = require('./file');\nconst Metrics = require('./metrics');\nconst Tor = require('./tor');\nconst Trending = require('./trending');\nconst User = require('./user');\nconst Views = require('./views');\n\nconst {\n  database,\n  username,\n  password,\n} = require('@config/mysqlConfig');\n\nif (!database || !username || !password) {\n  logger.warn('missing database, user, or password from mysqlConfig');\n}\n\n// set sequelize options\nconst sequelize = new Sequelize(database, username, password, {\n  host          : 'localhost',\n  dialect       : 'mysql',\n  dialectOptions: {\n    decimalNumbers: true,\n  },\n  logging: false,\n  pool   : {\n    max    : 5,\n    min    : 0,\n    idle   : 10000,\n    acquire: 10000,\n  },\n  operatorsAliases: false,\n});\n\n// establish mysql connection\nsequelize\n  .authenticate()\n  .then(() => {\n    logger.info('Sequelize has established mysql connection successfully.');\n  })\n  .catch(err => {\n    logger.error('Sequelize was unable to connect to the database:', err);\n  });\n\n// manually add each model to the db object (note: make this dynamic)\nconst db = {};\n\ndb['Blocked'] = Blocked(sequelize, Sequelize);\ndb['Certificate'] = Certificate(sequelize, Sequelize);\ndb['Channel'] = Channel(sequelize, Sequelize);\ndb['Claim'] = Claim(sequelize, Sequelize);\ndb['File'] = File(sequelize, Sequelize);\ndb['Metrics'] = Metrics(sequelize, Sequelize);\ndb['Tor'] = Tor(sequelize, Sequelize);\ndb['Trending'] = Trending(sequelize, Sequelize);\ndb['User'] = User(sequelize, Sequelize);\ndb['Views'] = Views(sequelize, Sequelize);\n\n// run model.association for each model in the db object that has an association\nlogger.info('associating db models...');\nObject.keys(db).forEach(modelName => {\n  if (db[modelName].associate) {\n    logger.info('Associating model:', modelName);\n    db[modelName].associate(db);\n  }\n});\n\n// add sequelize/Sequelize to db\ndb.sequelize = sequelize;\ndb.Sequelize = Sequelize;\n// add an 'upsert' method to the db object\ndb.upsert = (Model, values, condition, tableName) => {\n  return Model\n    .findOne({\n      where: condition,\n    })\n    .then(obj => {\n      if (obj) {  // update\n        logger.debug(`updating record in db.${tableName}`);\n        return obj.update(values);\n      } else {  // insert\n        logger.debug(`creating record in db.${tableName}`);\n        return Model.create(values);\n      }\n    })\n    .catch(function (error) {\n      logger.error(`${tableName}.upsert error`, error);\n      throw error;\n    });\n};\n\nmodule.exports = db;\n"
  },
  {
    "path": "server/models/metrics.js",
    "content": "module.exports = (sequelize, { BOOLEAN, DATE, STRING }) => {\n  const Metrics = sequelize.define(\n    'Metrics',\n    {\n      time: {\n        type        : DATE(6),\n        defaultValue: sequelize.NOW,\n      },\n      isInternal: {\n        type: BOOLEAN,\n      },\n      isChannel: {\n        type        : BOOLEAN,\n        defaultValue: false,\n      },\n      claimId: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      ip: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      request: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      userAgent: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      referrer: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      routePath: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      params: {\n        type        : STRING,\n        defaultValue: null,\n      },\n    },\n    {\n      freezeTableName: true,\n      timestamps     : false, // don't use default timestamps columns\n      indexes        : [\n        {\n          fields: ['isInternal', 'isChannel', 'time', 'claimId', 'routePath'],\n        },\n      ],\n    }\n  );\n\n  return Metrics;\n};\n"
  },
  {
    "path": "server/models/tor.js",
    "content": "const logger = require('winston');\nconst { details: { ipAddress } } = require('@config/siteConfig');\n\nmodule.exports = (sequelize, { STRING }) => {\n  const Tor = sequelize.define(\n    'Tor',\n    {\n      address: {\n        type     : STRING,\n        allowNull: false,\n      },\n      fingerprint: {\n        type     : STRING,\n        allowNull: true,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  Tor.refreshTable = function () {\n    let torList = [];\n    return fetch(`https://check.torproject.org/api/bulk?ip=${ipAddress}&port=80`)\n      .then(response => {\n        return response.json();\n      })\n      .then(jsonResponse => {\n        logger.debug('total tor nodes:', jsonResponse.length);\n        // prep the records\n        for (let i = 0; i < jsonResponse.length; i++) {\n          torList.push({\n            address    : jsonResponse[i].Address,\n            fingerprint: jsonResponse[i].Fingerprint,\n          });\n        }\n        // clear the table\n        return this.destroy({\n          truncate: true,\n        });\n      })\n      .then(() => {\n        // fill the table\n        return this.bulkCreate(torList);\n      })\n      .then(() => {\n        // return the new table\n        return this.findAll({\n          attributes: ['address', 'fingerprint'],\n          raw       : true,\n        });\n      })\n      .catch(error => {\n        throw error;\n      });\n  };\n\n  return Tor;\n};\n"
  },
  {
    "path": "server/models/trending.js",
    "content": "const chainquery = require('chainquery').default;\n\nmodule.exports = (sequelize, { BOOLEAN, DATE, FLOAT, INTEGER, STRING }) => {\n  const Trending = sequelize.define(\n    'Trending',\n    {\n      time: { /* TODO: Historical analysis and log roll */\n        type        : DATE(6),\n        defaultValue: sequelize.NOW,\n      },\n      isChannel: {\n        type        : BOOLEAN,\n        defaultValue: false,\n      },\n      claimId: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      publisherId: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      intervalViews: {\n        type        : INTEGER,\n        defaultValue: 0,\n      },\n      weight: {\n        type        : FLOAT,\n        defaultValue: 0,\n      },\n      zScore: {\n        type        : FLOAT,\n        defaultValue: 0,\n      },\n      pValue: {\n        type        : FLOAT,\n        defaultValue: 0,\n      },\n      // TODO: Calculate t-statistics\n    },\n    {\n      freezeTableName: true,\n      timestamps     : false, // don't use default timestamps columns\n      indexes        : [\n        {\n          fields: ['claimId'],\n        },\n        {\n          fields: ['time', 'isChannel', 'claimId', 'publisherId', 'weight'],\n        },\n      ],\n    }\n  );\n\n  Trending.getTrendingWeightData = async ({\n    hours = 2,\n    minutes = 0,\n    limit = 20,\n  } = {}) => {\n    let time = new Date();\n    time.setHours(time.getHours() - hours);\n    time.setMinutes(time.getMinutes() - minutes);\n\n    const sqlTime = time.toISOString().slice(0, 19).replace('T', ' ');\n\n    const selectString = 'DISTINCT(claimId), weight';\n    const whereString = `isChannel = false and time > '${sqlTime}'`;\n    const query = `SELECT ${selectString} FROM Trending WHERE ${whereString} ORDER BY weight DESC LIMIT ${limit}`;\n\n    return sequelize.query(query, { type: sequelize.QueryTypes.SELECT });\n  };\n\n  Trending.getTrendingClaims = async () => {\n    const trendingWeightData = await Trending.getTrendingWeightData();\n\n    const trendingClaimIds = [];\n    const trendingClaims = trendingWeightData.reduce((claims, trendingData) => {\n      trendingClaimIds.push(trendingData.claimId);\n      claims[trendingData.claimId] = {\n        ...trendingData,\n      };\n\n      return claims;\n    }, {});\n\n    const claimData = await chainquery.claim.findAll({\n      where: {\n        claim_id: { [sequelize.Op.in]: trendingClaimIds },\n      },\n    });\n\n    return claimData.map((claimData) => {\n      return Object.assign(trendingClaims[claimData.claim_id], claimData.dataValues);\n    });\n  };\n\n  return Trending;\n};\n"
  },
  {
    "path": "server/models/user.js",
    "content": "'use strict';\nconst bcrypt = require('bcrypt');\nconst logger = require('winston');\n\nmodule.exports = (sequelize, { STRING }) => {\n  const User = sequelize.define(\n    'User',\n    {\n      userName: {\n        type     : STRING,\n        allowNull: false,\n      },\n      password: {\n        type     : STRING,\n        allowNull: false,\n      },\n    },\n    {\n      freezeTableName: true,\n    }\n  );\n\n  User.associate = db => {\n    User.hasOne(db.Channel);\n  };\n\n  User.prototype.comparePassword = function (password) {\n    return bcrypt.compare(password, this.password);\n  };\n\n  User.prototype.changePassword = function (newPassword) {\n    return new Promise((resolve, reject) => {\n      // generate a salt string to use for hashing\n      bcrypt.genSalt((saltError, salt) => {\n        if (saltError) {\n          logger.error('salt error', saltError);\n          reject(saltError);\n          return;\n        }\n        // generate a hashed version of the user's password\n        bcrypt.hash(newPassword, salt, (hashError, hash) => {\n          // if there is an error with the hash generation return the error\n          if (hashError) {\n            logger.error('hash error', hashError);\n            reject(hashError);\n            return;\n          }\n          // replace the current password with the new hash\n          this\n            .update({password: hash})\n            .then(() => {\n              resolve();\n            })\n            .catch(error => {\n              reject(error);\n            });\n        });\n      });\n    });\n  };\n\n  // pre-save hook method to hash the user's password before the user's info is saved to the db.\n  User.addHook('beforeCreate', (user, options) => {\n    logger.debug('User.beforeCreate hook...');\n    return new Promise((resolve, reject) => {\n      // generate a salt string to use for hashing\n      bcrypt.genSalt((saltError, salt) => {\n        if (saltError) {\n          logger.error('salt error', saltError);\n          reject(saltError);\n          return;\n        }\n        // generate a hashed version of the user's password\n        bcrypt.hash(user.password, salt, (hashError, hash) => {\n          // if there is an error with the hash generation return the error\n          if (hashError) {\n            logger.error('hash error', hashError);\n            reject(hashError);\n            return;\n          }\n          // replace the password string with the hash password value\n          user.password = hash;\n          resolve();\n        });\n      });\n    });\n  });\n\n  return User;\n};\n"
  },
  {
    "path": "server/models/utils/createClaimRecordData.js",
    "content": "const db = require('../index.js');\n\nconst createClaimRecordDataAfterPublish = (\n  certificateId,\n  channelName,\n  fileName,\n  fileType,\n  publishParams,\n  publishResultsOutput\n) => {\n  const {\n    name,\n    title,\n    description,\n    thumbnail,\n    nsfw,\n    claim_address: address,\n    bid: amount,\n  } = publishParams;\n\n  const { claim_id: claimId, txid, nout } = publishResultsOutput;\n\n  return db.Claim.getCurrentHeight().then(height => {\n    return {\n      name,\n      claimId,\n      title,\n      description,\n      address,\n      thumbnail,\n      outpoint: `${txid}:${nout}`,\n      height,\n      contentType: fileType,\n      nsfw,\n      amount,\n      certificateId,\n      channelName,\n    };\n  });\n};\n\nmodule.exports = {\n  createClaimRecordDataAfterPublish,\n};\n"
  },
  {
    "path": "server/models/utils/createDatabaseIfNotExists.js",
    "content": "const Sequelize = require('sequelize');\nconst {database, username, password} = require('@config/mysqlConfig');\n\nconst createDatabaseIfNotExists = () => {\n  const sequelize = new Sequelize('', username, password, {\n    dialect         : 'mysql',\n    logging         : false,\n    operatorsAliases: false,\n  });\n  return new Promise((resolve, reject) => {\n    sequelize.query(`CREATE DATABASE IF NOT EXISTS ${database};`)\n      .then(() => {\n        resolve();\n      })\n      .catch(error => {\n        reject(error);\n      });\n  });\n};\n\nmodule.exports = createDatabaseIfNotExists;\n"
  },
  {
    "path": "server/models/utils/createFileRecordData.js",
    "content": "const getMediaDimensions = require('../../utils/getMediaDimensions.js');\n\nasync function createFileRecordDataAfterGet(resolveResult, getResult) {\n  const { name, claimId, outpoint, contentType: fileType } = resolveResult;\n\n  const { file_name: fileName, download_path: filePath } = getResult;\n\n  const { height: fileHeight, width: fileWidth } = await getMediaDimensions(fileType, filePath);\n\n  return {\n    name,\n    claimId,\n    outpoint,\n    fileHeight,\n    fileWidth,\n    fileName,\n    filePath,\n    fileType,\n  };\n}\n\nasync function createFileRecordDataAfterPublish(\n  fileName,\n  fileType,\n  publishParams,\n  publishResultsOutput\n) {\n  const { name, file_path: filePath } = publishParams;\n\n  const { claim_id: claimId, txid, nout } = publishResultsOutput;\n\n  const { height: fileHeight, width: fileWidth } = await getMediaDimensions(fileType, filePath);\n\n  return {\n    name,\n    claimId,\n    outpoint: `${txid}:${nout}`,\n    fileHeight,\n    fileWidth,\n    fileName,\n    filePath,\n    fileType,\n  };\n}\n\nmodule.exports = {\n  createFileRecordDataAfterGet,\n  createFileRecordDataAfterPublish,\n};\n"
  },
  {
    "path": "server/models/utils/returnShortId.js",
    "content": "const returnShortId = (claimsArray, longId) => {\n  let claimIndex;\n  let shortId = longId.substring(0, 1); // default short id is the first letter\n  let shortIdLength = 0;\n  // find the index of this claim id\n  claimIndex = claimsArray.findIndex(element => {\n    return element.claimId === longId;\n  });\n  if (claimIndex < 0) {\n    throw new Error('claim id not found in claims list');\n  }\n  // get an array of all claims with lower height\n  let possibleMatches = claimsArray.slice(0, claimIndex);\n  // remove certificates with the same prefixes until none are left.\n  while (possibleMatches.length > 0) {\n    shortIdLength += 1;\n    shortId = longId.substring(0, shortIdLength);\n    possibleMatches = possibleMatches.filter(element => {\n      return (element.claimId && (element.claimId.substring(0, shortIdLength) === shortId));\n    });\n  }\n  return shortId;\n};\n\nmodule.exports = returnShortId;\n"
  },
  {
    "path": "server/models/utils/returnShortId.test.js",
    "content": "const chai = require('chai');\nconst expect = chai.expect;\n\ndescribe('#parsePublishApiRequestBody()', function () {\n  const returnShortId = require('./returnShortId.js');\n  let dummyClaimsArray;\n  let dummyLongId;\n\n  it('should thow an error if the claimId is not in the claim list', function () {\n    dummyClaimsArray = [\n      {claimId: 'a123456789'},\n      {claimId: 'b123456789'},\n      {claimId: 'c123456789'},\n    ];\n    dummyLongId = 'xxxxxxxxxx';\n    expect(returnShortId.bind(this, dummyClaimsArray, dummyLongId)).to.throw();\n  });\n\n  it('should return the shortest unique claim id', function () {\n    dummyClaimsArray = [\n      {claimId: 'a123456789'},\n      {claimId: 'b123456789'},\n      {claimId: 'c123456789'},\n    ];\n    dummyLongId = 'c123456789';\n    expect(returnShortId(dummyClaimsArray, dummyLongId)).to.equal('c');\n  });\n\n  it('if there is a conflict between unqiue ids, it should give preference to the one with the lowest height', function () {\n    dummyClaimsArray = [\n      {claimId: 'a123456789', height: 10},\n      {claimId: 'ab12345678', height: 11},\n      {claimId: 'ab12341111', height: 12},\n    ];\n    dummyLongId = 'a123456789';\n    expect(returnShortId(dummyClaimsArray, dummyLongId)).to.equal('a');\n    dummyLongId = 'ab12345678';\n    expect(returnShortId(dummyClaimsArray, dummyLongId)).to.equal('ab');\n    dummyLongId = 'ab12341111';\n    expect(returnShortId(dummyClaimsArray, dummyLongId)).to.equal('ab12341');\n  });\n});\n"
  },
  {
    "path": "server/models/utils/trendingAnalysis.js",
    "content": "const ZSCORE_CRITICAL_THRESHOLD = 1.96; // 95-percentile\nconst ZSCORE_NINETYNINTH = 2.326347875; // 99-percentile\nconst ONE_DIV_SQRT_2PI = 0.3989422804014327; // V8 float of 1/SQRT(2 * PI)\nconst MAX_P_PRECISION = Math.exp(-16); // Rought estimation of V8 precision, -16 is 1.1253517471925912e-7\nconst MIN_P = -6.44357455534; // v8 float 0.0...0\nconst MAX_P = 6.44357455534; // v8 float 1.0...0\n\nconst getMean = (numArr) => {\n  let total = 0;\n  let length = numArr.length; // store local to reduce potential prop lookups\n\n  for (let i = 0; i < length; i++) {\n    total += numArr[i];\n  }\n\n  return total / length;\n};\n\nconst getStandardDeviation = (numArr, mean) => {\n  return Math.sqrt(numArr.reduce((sq, n) => (\n    sq + Math.pow(n - mean, 2)\n  ), 0) / (numArr.length - 1));\n};\n\nconst getInformationFromValues = (numArr) => {\n  let mean = getMean(numArr);\n\n  return {\n    mean,\n    standardDeviation: getStandardDeviation(numArr, mean),\n  };\n};\n\nconst getZScore = (value, mean, sDeviation) => (sDeviation !== 0 ? (value - mean) / sDeviation : 0);\n\nconst getFastPValue = (zScore) => {\n  if (zScore <= MIN_P) {\n    return 0;\n  }\n  if (zScore >= MAX_P) {\n    return 1;\n  }\n\n  let factorialK = 1;\n  let k = 0;\n  let sum = 0;\n  let term = 1;\n\n  while (Math.abs(term) > MAX_P_PRECISION) {\n    term = ONE_DIV_SQRT_2PI * Math.pow(-1, k) * Math.pow(zScore, k) / (2 * k + 1) / Math.pow(2, k) * Math.pow(zScore, k + 1) / factorialK;\n    sum += term;\n    k++;\n    factorialK *= k;\n  }\n  sum += 0.5;\n\n  return sum;\n};\n\nconst getWeight = (zScore, pValue) => (zScore * pValue);\n\nmodule.exports = {\n  getInformationFromValues,\n  getZScore,\n  getFastPValue,\n  getWeight,\n};\n"
  },
  {
    "path": "server/models/views.js",
    "content": "module.exports = (sequelize, { BOOLEAN, DATE, STRING }) => {\n  const Views = sequelize.define(\n    'Views',\n    {\n      time: {\n        type        : DATE(6),\n        defaultValue: sequelize.NOW,\n      },\n      isChannel: {\n        type        : BOOLEAN,\n        defaultValue: false,\n      },\n      claimId: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      publisherId: {\n        type        : STRING,\n        defaultValue: null,\n      },\n      ip: {\n        type        : STRING,\n        defaultValue: null,\n      },\n    },\n    {\n      freezeTableName: true,\n      timestamps     : false, // don't use default timestamps columns\n      indexes        : [\n        {\n          fields: ['time', 'isChannel', 'claimId', 'publisherId', 'ip'],\n        },\n      ],\n    }\n  );\n\n  Views.getUniqueViews = ({\n    hours = 0,\n    minutes = 30,\n  } = {}) => {\n    let time = new Date();\n    time.setHours(time.getHours() - hours);\n    time.setMinutes(time.getMinutes() - minutes);\n\n    const sqlTime = time.toISOString().slice(0, 19).replace('T', ' ');\n\n    const selectString = 'claimId, publisherId, isChannel, COUNT(DISTINCT ip) as views';\n    const groupString = 'claimId, publisherId, isChannel';\n\n    return sequelize.query(\n      `SELECT ${selectString} FROM Views WHERE time > '${sqlTime}' GROUP BY ${groupString}`,\n      { type: sequelize.QueryTypes.SELECT }\n    );\n  };\n\n  Views.getGetUniqueViewsbByClaimId = (claimId) => {\n    return Views.count({\n      where: {\n        claimId,\n      },\n      distinct: true,\n      col     : 'ip',\n    });\n  };\n\n  return Views;\n};\n"
  },
  {
    "path": "server/render/handleShowRender.jsx",
    "content": "import React from 'react';\nimport { renderToString } from 'react-dom/server';\nimport { createStore, applyMiddleware } from 'redux';\nimport { Provider } from 'react-redux';\nimport { StaticRouter } from 'react-router-dom';\nimport renderFullPage from './renderFullPage';\nimport createSagaMiddleware from 'redux-saga';\nimport { call } from 'redux-saga/effects';\nimport Helmet from 'react-helmet';\nimport * as httpContext from 'express-http-context';\nimport logger from 'winston';\n\nimport Reducers from '@reducers';\nimport GAListener from '@components/GAListener';\nimport App from '@app';\n\nconst createCanonicalLink = require('@globalutils/createCanonicalLink');\n\nconst getCanonicalUrlFromShow = show => {\n  const requestId = show.requestList[show.request.id];\n  const requestType = show.request.type;\n\n  if (!requestId || !requestType) {\n    return null;\n  }\n\n  switch (requestType) {\n    case 'ASSET_DETAILS':\n      const asset = show.assetList[requestId.key];\n      return createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});\n    case 'CHANNEL':\n      return createCanonicalLink({ channel: show.channelList[requestId.key] });\n    default:\n      return null;\n  }\n};\n\nconst returnSagaWithParams = (saga, params) => {\n  return function * () {\n    yield call(saga, params);\n  };\n};\n\nexport default (req, res) => {\n  let context = {};\n\n  const {\n    action = false,\n    saga = false,\n  } = httpContext.get('routeData');\n\n  if (action === 'fallback') {\n    res.status(404);\n  }\n\n  const runSaga = (action !== false && saga !== false);\n  const renderPage = (store) => {\n    // Workaround, remove when a solution for async httpContext exists\n    const showState = store.getState().show;\n    const assetKeys = Object.keys(showState.assetList);\n\n    if (assetKeys.length !== 0) {\n      res.claimId = showState.assetList[assetKeys[0]].claimId;\n    } else {\n      const channelKeys = Object.keys(showState.channelList);\n\n      if (channelKeys.length !== 0) {\n        res.claimId = showState.channelList[channelKeys[0]].longId;\n        res.isChannel = true;\n      }\n    }\n\n    // render component to a string\n    const html = renderToString(\n      <Provider store={store}>\n        <StaticRouter location={req.url} context={context}>\n          <GAListener>\n            <App />\n          </GAListener>\n        </StaticRouter>\n      </Provider>\n    );\n\n    // get head tags from helmet\n    const helmet = Helmet.renderStatic();\n\n    // check for a redirect\n    if (context.url) {\n      return res.redirect(301, context.url);\n    }\n\n    // get the initial state from our Redux store\n    const preloadedState = store.getState();\n\n    // send the rendered page back to the client\n\n    res.send(renderFullPage(helmet, html, preloadedState));\n  };\n\n  if (runSaga) {\n    // create and apply middleware\n    const sagaMiddleware = createSagaMiddleware();\n    const middleware = applyMiddleware(sagaMiddleware);\n\n    // create a new Redux store instance\n    const store = createStore(Reducers, middleware);\n\n    // create an action to handle the given url,\n    // and create a the saga needed to handle that action\n    const boundAction = action(req.params, req.url);\n    const boundSaga = returnSagaWithParams(saga, boundAction);\n\n    // run the saga middleware with the saga call\n    sagaMiddleware\n      .run(boundSaga)\n      .done\n      .then(() => {\n        // redirect if request does not use canonical url\n        const canonicalUrl = getCanonicalUrlFromShow(store.getState().show);\n\n        if (!canonicalUrl) {\n          res.status(404);\n        }\n\n        if (canonicalUrl && canonicalUrl !== req.originalUrl) {\n          logger.verbose(`redirecting ${req.originalUrl} to ${canonicalUrl}`);\n          return res.redirect(canonicalUrl);\n        }\n\n        return renderPage(store);\n      });\n  } else {\n    const store = createStore(Reducers);\n    renderPage(store);\n  }\n};\n"
  },
  {
    "path": "server/render/renderFullPage.js",
    "content": "const md5File = require('md5-file');\nconst path = require('path');\n\nconst bundlePath = path.resolve('./public/bundle/bundle.js');\nconst bundleHash = md5File.sync(bundlePath);\nconst shortBundleHash = bundleHash.substring(0, 4);\n\nmodule.exports = (helmet, html, preloadedState) => {\n  // take the html and preloadedState and return the full page\n  return `\n    <!DOCTYPE html>\n    <html lang=\"en\" prefix=\"og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#\">\n        <head>\n            <meta charset=\"UTF-8\">\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no\">\n            <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n            <meta name=\"google-site-verification\" content=\"U3240KfVplLZSRCcOHxGuDFQO6eVUXKeFsSD2WJvdLo\" />\n            <!--helmet-->\n            ${helmet.title.toString()}\n            ${helmet.meta.toString()}\n            ${helmet.link.toString()}\n            <!--style sheets-->\n            <link rel=\"stylesheet\" href=\"/bundle/style.css\" type=\"text/css\">\n            <!--google font-->\n            <link href=\"https://fonts.googleapis.com/css?family=Roboto:300\" rel=\"stylesheet\">\n            <link href=\"https://fonts.googleapis.com/css?family=Lora\" rel=\"stylesheet\">\n        </head>\n        <body>\n            <div id=\"react-app\">${html}</div>\n            <script>\n                window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(\n                  /</g,\n                  '\\\\\\u003c'\n                )}\n            </script>\n            <script src=\"/bundle/bundle.js?${shortBundleHash}\"></script>\n        </body>\n    </html>\n  `;\n};\n"
  },
  {
    "path": "server/routes/api/index.js",
    "content": "// middleware\nconst { autoblockPublishMiddleware, autoblockPublishBodyMiddleware } = require('../../middleware/autoblockPublishMiddleware');\nconst multipartMiddleware = require('../../middleware/multipartMiddleware');\nconst torCheckMiddleware = require('../../middleware/torCheckMiddleware');\n// route handlers\nconst channelAvailability = require('../../controllers/api/channel/availability');\nconst channelClaims = require('../../controllers/api/channel/claims');\nconst channelData = require('../../controllers/api/channel/data');\nconst channelShortId = require('../../controllers/api/channel/shortId');\nconst claimAvailability = require('../../controllers/api/claim/availability');\nconst claimData = require('../../controllers/api/claim/data/');\nconst claimGet = require('../../controllers/api/claim/get');\nconst claimList = require('../../controllers/api/claim/list');\nconst claimLongId = require('../../controllers/api/claim/longId');\nconst claimPublish = require('../../controllers/api/claim/publish');\nconst claimAbandon = require('../../controllers/api/claim/abandon');\nconst claimUpdate = require('../../controllers/api/claim/update');\nconst claimResolve = require('../../controllers/api/claim/resolve');\nconst claimShortId = require('../../controllers/api/claim/shortId');\nconst claimViews = require('../../controllers/api/claim/views');\nconst fileAvailability = require('../../controllers/api/file/availability');\nconst specialClaims = require('../../controllers/api/special/claims');\nconst userPassword = require('../../controllers/api/user/password');\nconst publishingConfig = require('../../controllers/api/config/site/publishing');\nconst getTorList = require('../../controllers/api/tor');\nconst getBlockedList = require('../../controllers/api/blocked');\nconst getOEmbedData = require('../../controllers/api/oEmbed');\nconst cors = require('cors');\n\nexport default {\n  // homepage routes\n  '/api/homepage/data/channels'                           : { controller: [ torCheckMiddleware, channelData ] },\n  // channel routes\n  '/api/channel/availability/:name'                       : { controller: [ torCheckMiddleware, channelAvailability ] },\n  '/api/channel/short-id/:longId/:name'                   : { controller: [ torCheckMiddleware, channelShortId ] },\n  '/api/channel/data/:channelName/:channelClaimId'        : { controller: [ torCheckMiddleware, channelData ] },\n  '/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },\n\n  // sepcial routes\n  '/api/special/:name/:page': { controller: [ torCheckMiddleware, specialClaims ] },\n\n  // claim routes\n  '/api/claim/availability/:name'        : { controller: [ torCheckMiddleware, claimAvailability ] },\n  '/api/claim/data/:claimName/:claimId'  : { controller: [ torCheckMiddleware, claimData ] },\n  '/api/claim/get/:name/:claimId'        : { controller: [ torCheckMiddleware, claimGet ] },\n  '/api/claim/list/:name'                : { controller: [ torCheckMiddleware, claimList ] },\n  '/api/claim/long-id'                   : { method: 'post', controller: [ cors(), torCheckMiddleware, claimLongId ] }, // note: should be a 'get'\n  '/api/claim/publish'                   : { method: 'post', controller: [ cors(), torCheckMiddleware, autoblockPublishMiddleware, multipartMiddleware, autoblockPublishBodyMiddleware, claimPublish ] },\n  '/api/claim/update'                    : { method: 'post', controller: [ cors(), torCheckMiddleware, multipartMiddleware, claimUpdate ] },\n  '/api/claim/abandon'                   : { method: 'post', controller: [ cors(), torCheckMiddleware, multipartMiddleware, claimAbandon ] },\n  '/api/claim/resolve/:name/:claimId'    : { controller: [ torCheckMiddleware, claimResolve ] },\n  '/api/claim/short-id/:longId/:name'    : { controller: [ torCheckMiddleware, claimShortId ] },\n  '/api/claim/views/:claimId'            : { controller: [ torCheckMiddleware, claimViews ] },\n  // file routes\n  '/api/file/availability/:name/:claimId': { controller: [ torCheckMiddleware, fileAvailability ] },\n  // user routes\n  '/api/user/password/'                  : { method: 'put', controller: [ torCheckMiddleware, userPassword ] },\n  // configs\n  '/api/config/site/publishing'          : { controller: [ cors(), torCheckMiddleware, publishingConfig ] },\n  // tor\n  '/api/tor'                             : { controller: [ torCheckMiddleware, getTorList ] },\n  // blocked\n  '/api/blocked'                         : { controller: [ torCheckMiddleware, getBlockedList ] },\n  // open embed\n  '/api/oembed'                          : { controller: [ torCheckMiddleware, getOEmbedData ] },\n};\n"
  },
  {
    "path": "server/routes/assets/index.js",
    "content": "const serveByClaim = require('../../controllers/assets/serveByClaim');\nconst serveByIdentifierAndClaim = require('../../controllers/assets/serveByIdentifierAndClaim');\n\n// TODO: Adjust build & sources to use import/export everywhere\nconst Actions = require('@actions').default;\nconst Sagas = require('@sagas').default;\n\nexport default {\n  '/:identifier/:claim': { controller: serveByIdentifierAndClaim, action: Actions.onHandleShowPageUri, saga: Sagas.handleShowPageUri },\n  '/:claim'            : { controller: serveByClaim, action: Actions.onHandleShowPageUri, saga: Sagas.handleShowPageUri },\n};\n"
  },
  {
    "path": "server/routes/auth/index.js",
    "content": "const speechPassport = require('../../speechPassport');\nconst handleSignupRequest = require('../../controllers/auth/signup');\nconst handleLoginRequest = require('../../controllers/auth/login');\nconst handleLogoutRequest = require('../../controllers/auth/logout');\nconst handleUserRequest = require('../../controllers/auth/user');\n\nexport default {\n  '/signup': { method: 'post', controller: [ speechPassport.authenticate('local-signup'), handleSignupRequest ] },\n  '/auth'  : { method: 'post', controller: handleLoginRequest },\n  '/logout': { controller: handleLogoutRequest },\n  '/user'  : { controller: handleUserRequest },\n};\n"
  },
  {
    "path": "server/routes/fallback/index.js",
    "content": "import handlePageRequest from '../../controllers/pages/sendReactApp';\n\nexport default {\n  '*': { controller: handlePageRequest, action: 'fallback' },\n};\n"
  },
  {
    "path": "server/routes/index.js",
    "content": "module.exports = {\n  ...require('./pages').default,\n  ...require('./api').default,\n  ...require('./auth').default,\n  // ...require('./assets').default,\n  ...require('./fallback').default,\n};\n"
  },
  {
    "path": "server/routes/pages/index.js",
    "content": "import handlePageRequest from '../../controllers/pages/sendReactApp';\nconst handleVideoEmbedRequest = require('../../controllers/pages/sendVideoEmbedPage');\nconst redirect = require('../../controllers/utils/redirect');\n\n// TODO: Adjust build & sources to use import/export everywhere\nconst Actions = require('@actions').default;\nconst Sagas = require('@sagas').default;\n\nexport default {\n  '/'                                   : { controller: handlePageRequest, action: Actions.onHandleShowHomepage, saga: Sagas.handleShowHomepage  },\n  '/login'                              : { controller: handlePageRequest },\n  '/about'                              : { controller: handlePageRequest },\n  '/tos'                                : { controller: handlePageRequest },\n  '/faq'                                : { controller: handlePageRequest },\n  '/trending'                           : { controller: redirect('/popular') },\n  '/popular'                            : { controller: handlePageRequest },\n  '/new'                                : { controller: handlePageRequest },\n  '/edit/:claimId'                      : { controller: handlePageRequest },\n  '/multisite'                          : { controller: handlePageRequest },\n  '/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest },  // for twitter\n};\n"
  },
  {
    "path": "server/speechPassport/index.js",
    "content": "const passport = require('passport');\nconst localLoginStrategy = require('./utils/local-login.js');\nconst localSignupStrategy = require('./utils/local-signup.js');\nconst serializeUser = require('./utils/serializeUser.js');\nconst deserializeUser = require('./utils/deserializeUser.js');\n\npassport.deserializeUser(deserializeUser);\npassport.serializeUser(serializeUser);\npassport.use('local-login', localLoginStrategy);\npassport.use('local-signup', localSignupStrategy);\n\nmodule.exports = passport;\n"
  },
  {
    "path": "server/speechPassport/utils/deserializeUser.js",
    "content": "const deserializeUser = (user, done) => {\n  // deserializes session and populates additional info to req.user\n  done(null, user);\n};\n\nmodule.exports = deserializeUser;\n"
  },
  {
    "path": "server/speechPassport/utils/local-login.js",
    "content": "const PassportLocalStrategy = require('passport-local').Strategy;\nconst logger = require('winston');\nconst db = require('../../models');\n\nconst returnUserAndChannelInfo = (userInstance) => {\n  return new Promise((resolve, reject) => {\n    let userInfo = {};\n    userInfo['id'] = userInstance.id;\n    userInfo['userName'] = userInstance.userName;\n    userInstance\n      .getChannel()\n      .then(({channelName, channelClaimId}) => {\n        userInfo['channelName'] = channelName;\n        userInfo['channelClaimId'] = channelClaimId;\n        return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);\n      })\n      .then(shortChannelId => {\n        userInfo['shortChannelId'] = shortChannelId;\n        resolve(userInfo);\n      })\n      .catch(error => {\n        reject(error);\n      });\n  });\n};\n\nmodule.exports = new PassportLocalStrategy(\n  {\n    usernameField: 'username',\n    passwordField: 'password',\n  },\n  (username, password, done) => {\n    return db.User\n      .findOne({\n        where: {userName: username},\n      })\n      .then(user => {\n        if (!user) {\n          logger.debug('no user found');\n          return done(null, false, {message: 'Incorrect username or password'});\n        }\n        return user.comparePassword(password)\n          .then(isMatch => {\n            if (!isMatch) {\n              logger.debug('incorrect password');\n              return done(null, false, {message: 'Incorrect username or password'});\n            }\n            logger.debug('Password was a match, returning User');\n            return returnUserAndChannelInfo(user)\n              .then(userInfo => {\n                return done(null, userInfo);\n              })\n              .catch(error => {\n                return error;\n              });\n          })\n          .catch(error => {\n            return error;\n          });\n      })\n      .catch(error => {\n        return done(error);\n      });\n  }\n);\n"
  },
  {
    "path": "server/speechPassport/utils/local-signup.js",
    "content": "const PassportLocalStrategy = require('passport-local').Strategy;\nconst { createChannel } = require('../../lbrynet');\nconst logger = require('winston');\nconst db = require('../../models');\nconst {\n  publishing: { closedRegistration },\n} = require('@config/siteConfig');\n\nmodule.exports = new PassportLocalStrategy(\n  {\n    usernameField: 'username',\n    passwordField: 'password',\n  },\n  (username, password, done) => {\n    if (closedRegistration) {\n      return done('Registration is disabled');\n    }\n\n    logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`);\n    let userInfo = {};\n    // server-side validaton of inputs (username, password)\n    // create the channel and retrieve the metadata\n    return createChannel(`@${username}`)\n      .then(tx => {\n        // create user record\n        const userData = {\n          userName: username,\n          password: password,\n        };\n        logger.verbose('userData >', userData);\n        // create user record\n        const channelData = {\n          channelName: `@${username}`,\n          channelClaimId: tx.outputs[0].claim_id,\n        };\n        logger.verbose('channelData >', channelData);\n        // create certificate record\n        const certificateData = {\n          claimId: tx.outputs[0].claim_id,\n          name: `@${username}`,\n          // address,\n        };\n        logger.verbose('certificateData >', certificateData);\n        // save user and certificate to db\n        return Promise.all([\n          db.User.create(userData),\n          db.Channel.create(channelData),\n          db.Certificate.create(certificateData),\n        ]);\n      })\n      .then(([newUser, newChannel, newCertificate]) => {\n        logger.verbose('user and certificate successfully created');\n        // store the relevant newUser info to be passed back for req.User\n        userInfo['id'] = newUser.id;\n        userInfo['userName'] = newUser.userName;\n        userInfo['channelName'] = newChannel.channelName;\n        userInfo['channelClaimId'] = newChannel.channelClaimId;\n        // associate the instances\n        return Promise.all([newCertificate.setChannel(newChannel), newChannel.setUser(newUser)]);\n      })\n      .then(() => {\n        logger.verbose('user and certificate successfully associated');\n        return db.Certificate.getShortChannelIdFromLongChannelId(\n          userInfo.channelClaimId,\n          userInfo.channelName\n        );\n      })\n      .then(shortChannelId => {\n        userInfo['shortChannelId'] = shortChannelId;\n        return done(null, userInfo);\n      })\n      .catch(error => {\n        logger.error('signup error', error);\n        return done(error);\n      });\n  }\n);\n"
  },
  {
    "path": "server/speechPassport/utils/serializeUser.js",
    "content": "const serializeUser = (user, done) => {\n  // returns user data to be serialized into session\n  done(null, user);\n};\n\nmodule.exports = serializeUser;\n"
  },
  {
    "path": "server/task-scripts/update-channel-names.js",
    "content": "// load dependencies\nconst logger = require('winston');\nconst db = require('../models');\nrequire('../helpers/configureLogger.js')(logger);\n\nlet totalClaims = 0;\nlet totalClaimsNoCertificate = 0;\n\ndb.sequelize.sync() // sync sequelize\n  .then(() => {\n    logger.info('finding claims with no channels');\n    return db.Claim.findAll({\n      where: {\n        channelName  : null,\n        certificateId: {\n          $ne: null,\n        },\n      },\n    });\n  })\n  .then(claimsArray => {\n    totalClaims = claimsArray.length;\n    const claimsUpdatePromises = claimsArray.map(claim => {\n      return db.Certificate\n        .findOne({\n          where: { claimId: claim.get('certificateId') },\n        })\n        .then(certificate => {\n          // if a certificate is found...\n          if (certificate) {\n            logger.debug('certificate found');\n            // update the claim's channel name with the certificate's name\n            return claim\n              .update({\n                channelName: certificate.get('name'),\n              })\n              .then(() => {})\n              .catch(error => logger.error(error));\n          }\n          logger.warn('no record found for certificate: ', claim.get('certificateId'));\n          totalClaimsNoCertificate += 1;\n        })\n        .catch(error => logger.error(error));\n    });\n    return Promise.all(claimsUpdatePromises);\n  })\n  .then(() => {\n    logger.info('total claims found', totalClaims);\n    logger.info('total claims found with no matching certificate record', totalClaimsNoCertificate);\n    logger.debug('all done');\n  })\n  .catch((error) => {\n    logger.error(error);\n  });\n"
  },
  {
    "path": "server/task-scripts/update-password.js",
    "content": "// load dependencies\nconst logger = require('winston');\nconst db = require('../models');\n// configure logging\nrequire('../helpers/configureLogger.js')(logger);\n\nconst userName = process.argv[2];\nlogger.debug('user name:', userName);\nconst oldPassword = process.argv[3];\nlogger.debug('old password:', oldPassword);\nconst newPassword = process.argv[4];\nlogger.debug('new password:', newPassword);\n\ndb.sequelize.sync() // sync sequelize\n  .then(() => {\n    logger.info('finding user profile');\n    return db.User.findOne({\n      where: {\n        userName: userName,\n      },\n    });\n  })\n  .then(user => {\n    if (!user) {\n      throw new Error('no user found');\n    }\n    return Promise.all([\n      user.comparePassword(oldPassword),\n      user,\n    ]);\n  })\n  .then(([isMatch, user]) => {\n    if (!isMatch) {\n      throw new Error('Incorrect old password.');\n    }\n    logger.debug('Password was a match, updating password');\n    return user.changePassword(newPassword);\n  })\n  .then(() => {\n    logger.debug('Password successfully updated');\n  })\n  .catch((error) => {\n    logger.error(error);\n  });\n"
  },
  {
    "path": "server/utils/awaitFileSize.js",
    "content": "const { getFileListFileByOutpoint } = require('server/lbrynet');\nconst logger = require('winston');\n\nfunction delay(t) {\n  return new Promise(function(resolve) {\n    setTimeout(resolve, t);\n  });\n}\n\nconst awaitFileSize = (outpoint, size, interval, timeout) => {\n  logger.debug('awaitFileSize');\n  let start = Date.now();\n  function checkFileList() {\n    logger.debug('checkFileList');\n    return getFileListFileByOutpoint(outpoint).then(result => {\n      const { items: fileInfos } = result;\n      const fileInfo = fileInfos[0];\n      logger.debug('File List Result', fileInfo);\n      if (fileInfo.completed === true || fileInfo.written_bytes > size) {\n        logger.debug('FILE READY');\n        return 'ready';\n      } else if (timeout !== 0 && Date.now() - start > timeout) {\n        throw new Error('Timeout on awaitFileSize');\n      } else {\n        return delay(interval).then(checkFileList);\n      }\n    });\n  }\n  return checkFileList();\n};\n\nmodule.exports = awaitFileSize;\n"
  },
  {
    "path": "server/utils/blockList.js",
    "content": "const logger = require('winston');\nconst db = require('../models');\n\nlet blockList = new Set();\n\nconst setupBlockList = (intervalInSeconds = 60) => {\n  const fetchList = () => {\n    return new Promise((resolve, reject) => {\n      db.Blocked.getBlockList()\n        .then((result) => {\n          blockList.clear();\n          if (result.length > 0) {\n            result.map((item) => { blockList.add(item.dataValues.outpoint) });\n            resolve();\n          } else reject();\n        })\n        .catch(e => { console.error('list was empty', e) });\n    });\n  };\n  setInterval(() => { fetchList() }, intervalInSeconds * 1000);\n  return fetchList();\n};\nmodule.exports = {\n  isBlocked: (outpoint) => { return blockList.has(outpoint) },\n  setupBlockList,\n};\n"
  },
  {
    "path": "server/utils/configureLogging.js",
    "content": "const logger = require('winston');\n\nconst config = require('@config/loggerConfig');\nconst { logLevel } = config;\n\nfunction configureLogging() {\n  logger.info('configuring winston logger...');\n  if (!config) {\n    return logger.warn('No logger config found');\n  }\n  if (!logLevel) {\n    logger.warn('No logLevel found in config.');\n  }\n  // configure the winston logger\n  logger.configure({\n    transports: [\n      new logger.transports.Console({\n        level: logLevel || 'debug',\n        timestamp: true,\n        colorize: true,\n        prettyPrint: true,\n        handleExceptions: true,\n        humanReadableUnhandledException: true,\n      }),\n    ],\n  });\n  // test all the log levels\n  logger.info('testing winston log levels...');\n  logger.warn('Testing: Log Level 1');\n  logger.info('Testing: Log Level 2');\n  logger.verbose('Testing: Log Level 3');\n  logger.debug('Testing: Log Level 4');\n  logger.silly('Testing: Log Level 5');\n}\n\nmodule.exports = configureLogging;\n"
  },
  {
    "path": "server/utils/configureSlack.js",
    "content": "const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook;\nconst logger = require('winston');\n\nconst config = require('@config/slackConfig');\nconst {slackWebHook, slackErrorChannel, slackInfoChannel} = config;\n\nfunction configureSlack () {\n  logger.info('configuring slack logger...');\n  if (!config) {\n    return logger.warn('No slack config found');\n  }\n  // update slack webhook settings\n  if (!slackWebHook) {\n    return logger.info('Slack logging is not enabled because no slackWebHook config var provided.');\n  }\n  // add a transport for errors to slack\n  if (slackErrorChannel) {\n    logger.add(winstonSlackWebHook, {\n      name      : 'slack-errors-transport',\n      level     : 'warn',\n      webhookUrl: slackWebHook,\n      channel   : slackErrorChannel,\n      username  : 'spee.ch',\n      iconEmoji : ':face_with_head_bandage:',\n    });\n  } else {\n    logger.warn('No slack error channel logging set up');\n  }\n  // add a transport for info in slack\n  if (slackInfoChannel) {\n    logger.add(winstonSlackWebHook, {\n      name      : 'slack-info-transport',\n      level     : 'info',\n      webhookUrl: slackWebHook,\n      channel   : slackInfoChannel,\n      username  : 'spee.ch',\n      iconEmoji : ':nerd_face:',\n    });\n  } else {\n    logger.warn('No slack info channel logging set up');\n  }\n  // send test messages\n  logger.info('Slack logging is online.');\n}\n\nmodule.exports = configureSlack;\n"
  },
  {
    "path": "server/utils/createModuleAliases.js",
    "content": "const { resolve } = require('path');\nconst WWW_SPEECH_ROOT = resolve(process.cwd());\n\nmodule.exports = () => {\n  let moduleAliases = {};\n  // default aliases\n  moduleAliases['@config'] = resolve(WWW_SPEECH_ROOT, 'config');\n  moduleAliases['@public'] = resolve(WWW_SPEECH_ROOT, 'public');\n  // return finished aliases\n  return moduleAliases;\n};\n"
  },
  {
    "path": "server/utils/fetchClaimData.js",
    "content": "const chainquery = require('chainquery').default;\nconst db = require('server/models');\n\nconst fetchClaimData = async params => {\n  let { claimId, claimName: name } = params;\n  if (claimId === 'none') claimId = null;\n\n  const [cq, local] = await Promise.all([\n    chainquery.claim.queries.resolveClaim(name, claimId).catch(() => {}),\n    db.Claim.resolveClaim(name, claimId).catch(() => {}),\n  ]);\n  // Todo: don't use localdb to get post publish content\n  if (!cq && !local) {\n    return null;\n  }\n  if (cq && cq.name === name && !local) {\n    return cq;\n  }\n  if (local && local.name === name && !cq) {\n    return local;\n  }\n  return local.updatedAt > cq.modified_at ? local : cq;\n};\n\nmodule.exports = fetchClaimData;\n"
  },
  {
    "path": "server/utils/getClaimData.js",
    "content": "const {\n  details: { host },\n  assetDefaults: { thumbnail },\n} = require('@config/siteConfig');\nconst chainquery = require('chainquery').default;\n// const { getClaim } = require('server/lbrynet');\nconst { isBlocked } = require('./blockList');\n\nmodule.exports = async (data, chName = null, chShortId = null) => {\n  // TODO: Refactor getching the channel name out; requires invasive changes.\n  const dataVals = data.dataValues ? data.dataValues : data;\n  const txid = dataVals.transaction_hash_id || dataVals.txid;\n  let nout;\n\n  if (typeof dataVals.vout === 'number') {\n    nout = dataVals.vout;\n  } else {\n    nout = dataVals.nout;\n  }\n\n  const outpoint = `${txid}:${nout}`;\n  const certificateId = dataVals.publisher_id || dataVals.certificateId;\n  const fileExt = data.generated_extension || dataVals.fileExt;\n\n  let channelShortId = chShortId;\n  let channelName = chName;\n  // TODO: Factor blocked out\n  let blocked;\n\n  if (isBlocked(outpoint)) {\n    blocked = true;\n  }\n\n  if (!chName && certificateId && !channelName) {\n    channelName = await chainquery.claim.queries.getClaimChannelName(certificateId).catch(() => {});\n  }\n\n  if (!chShortId && certificateId && channelName) {\n    channelShortId = await chainquery.claim.queries\n      .getShortClaimIdFromLongClaimId(certificateId, channelName)\n      .catch(() => null);\n  }\n\n  // Find a solution for the legacy application/octet-stream file extensions\n\n  return {\n    name: dataVals.name,\n    title: dataVals.title,\n    certificateId,\n    channelName,\n    channelShortId,\n    contentType: dataVals.content_type || data.contentType,\n    claimId: dataVals.claim_id || data.claimId,\n    fileExt: fileExt,\n    description: dataVals.description,\n    nsfw: dataVals.is_nsfw,\n    thumbnail: dataVals.thumbnail_url || data.thumbnail || thumbnail,\n    outpoint,\n    host,\n    pending: Boolean(dataVals.height === 0),\n    blocked: blocked,\n    license: dataVals.license,\n    licenseUrl: dataVals.license_url,\n    transactionTime: dataVals.transaction_time,\n  };\n};\n"
  },
  {
    "path": "server/utils/getMediaDimensions.js",
    "content": "const logger = require('winston');\nconst { getImageHeightAndWidth } = require('./imageProcessing');\nconst { getVideoHeightAndWidth } = require('./videoProcessing');\n\nasync function getMediaDimensions (fileType, filePath) {\n  let height = 0;\n  let width = 0;\n  switch (fileType) {\n    case 'image/jpeg':\n    case 'image/jpg':\n    case 'image/png':\n    case 'image/gif':\n      logger.debug('creating File data for an image');\n      [ height, width ] = await getImageHeightAndWidth(filePath);\n      break;\n    case 'video/mp4':\n      logger.debug('creating File data for a video');\n      [ height, width ] = await getVideoHeightAndWidth(filePath);\n      break;\n    default:\n      logger.error('unable to create File dimension data for unspported file type:', fileType);\n      break;\n  }\n  return {\n    height,\n    width,\n  };\n}\n\nmodule.exports = getMediaDimensions;\n"
  },
  {
    "path": "server/utils/googleAnalytics.js",
    "content": "const logger = require('winston');\nconst ua = require('universal-analytics');\nconst { analytics : { googleId }, details: { title } } = require('@config/siteConfig');\n\nconst createServeEventParams = (headers, ip, originalUrl) => {\n  return {\n    eventCategory    : 'client requests',\n    eventAction      : 'serve request',\n    eventLabel       : originalUrl,\n    ipOverride       : ip,\n    userAgentOverride: headers['user-agent'],\n    documentReferrer : headers['referer'],\n  };\n};\n\nconst createTimingEventParams = (category, variable, label, startTime, endTime) => {\n  const duration = endTime - startTime;\n  return {\n    userTimingCategory    : category,\n    userTimingVariableName: variable,\n    userTimingTime        : duration,\n    userTimingLabel       : label,\n  };\n};\n\nconst sendGoogleAnalyticsEvent = (ip, params) => {\n  if (!googleId) {\n    return logger.debug('Skipping analytics event because no GoogleId present in configs');\n  }\n  const visitorId = ip.replace(/\\./g, '-');\n  const visitor = ua(googleId, visitorId, { strictCidFormat: false, https: true });\n  visitor.event(params, (err) => {\n    if (err) {\n      return logger.error('Google Analytics Event Error >>', err);\n    }\n    logger.debug(`Event successfully sent to google analytics`, params);\n  });\n};\n\nconst sendGoogleAnalyticsTiming = (siteTitle, params) => {\n  if (!googleId) {\n    return logger.debug('Skipping analytics timing because no GoogleId present in configs');\n  }\n  const visitor = ua(googleId, siteTitle, { strictCidFormat: false, https: true });\n  visitor.timing(params, (err) => {\n    if (err) {\n      return logger.error('Google Analytics Event Error >>', err);\n    }\n    logger.debug(`Event successfully sent to google analytics`, params);\n  });\n};\n\nconst sendGAServeEvent = (headers, ip, originalUrl) => {\n  const params = createServeEventParams(headers, ip, originalUrl);\n  sendGoogleAnalyticsEvent(ip, params);\n};\n\nconst sendGATimingEvent = (category, variable, label, startTime, endTime) => {\n  const params = createTimingEventParams(category, variable, label, startTime, endTime);\n  sendGoogleAnalyticsTiming(title, params);\n};\n\nconst chooseGaLbrynetPublishLabel = ({ channel_name: channelName, channel_id: channelId }) => {\n  return (channelName || channelId ? 'PUBLISH_IN_CHANNEL_CLAIM' : 'PUBLISH_ANONYMOUS_CLAIM');\n};\n\nmodule.exports = {\n  sendGAServeEvent,\n  sendGATimingEvent,\n  chooseGaLbrynetPublishLabel,\n};\n"
  },
  {
    "path": "server/utils/imageProcessing.js",
    "content": "const sizeOf = require('image-size');\n\nconst getImageHeightAndWidth = (filePath) => {\n  return new Promise((resolve, reject) => {\n    try {\n      const { height, width } = sizeOf(filePath);\n      resolve([height, width]);\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nmodule.exports = {\n  getImageHeightAndWidth,\n};\n"
  },
  {
    "path": "server/utils/isRequestLocal.js",
    "content": "module.exports = function (req) {\n  let reqIp = req.connection.remoteAddress;\n  let host = req.get('host');\n\n  return reqIp === '127.0.0.1' || reqIp === '::ffff:127.0.0.1' || reqIp === '::1' || host.indexOf('localhost') !== -1;\n};\n"
  },
  {
    "path": "server/utils/isValidQueryObj.js",
    "content": "const {\n  serving: { dynamicFileSizing },\n} = require('@config/siteConfig');\nconst { maxDimension } = dynamicFileSizing;\n\nconst isValidQueryObj = queryObj => {\n  let {\n    h: cHeight = null,\n    w: cWidth = null,\n    t: transform = null,\n    x: xOrigin = null,\n    y: yOrigin = null,\n  } = queryObj;\n\n  return (\n    ((cHeight <= maxDimension && cHeight > 0) || cHeight === null) &&\n    ((cWidth <= maxDimension && cWidth > 0) || cWidth === null) &&\n    (transform === null || transform === 'crop' || transform === 'stretch') &&\n    ((xOrigin <= maxDimension && xOrigin >= 0) || xOrigin === null) &&\n    ((yOrigin <= maxDimension && yOrigin >= 0) || yOrigin === null)\n  );\n};\n\nmodule.exports = isValidQueryObj;\n"
  },
  {
    "path": "server/utils/processTrending.js",
    "content": "const db = require('server/models');\nconst {\n  getInformationFromValues,\n  getZScore,\n  getFastPValue,\n  getWeight,\n} = require('server/models/utils/trendingAnalysis');\n\nconst logger = require('winston');\n\nmodule.exports = async () => {\n  try {\n    const claims = await db.Trending.getTrendingClaims();\n    const claimViews = await db.Views.getUniqueViews();\n\n    if (claimViews.length <= 1) {\n      return;\n    }\n\n    const time = Date.now();\n\n    // Must create statistical analytics before we can process zScores, etc\n    const viewsNumArray = claimViews.map((claimViewsEntry) => claimViewsEntry.views);\n    const {\n      mean,\n      standardDeviation,\n    } = getInformationFromValues(viewsNumArray);\n\n    for (let i = 0; i < claimViews.length; i++) {\n      let claimViewsEntry = claimViews[i];\n\n      const {\n        isChannel,\n        claimId,\n        publisherId,\n      } = claimViewsEntry;\n\n      const zScore = getZScore(claimViewsEntry.views, mean, standardDeviation);\n      const pValue = getFastPValue(zScore);\n      const weight = getWeight(zScore, pValue);\n\n      const trendingData = {\n        time,\n        isChannel    : claimViewsEntry.isChannel,\n        claimId      : claimViewsEntry.claimId,\n        publisherId  : claimViewsEntry.publisherId,\n        intervalViews: claimViewsEntry.views,\n        weight,\n        zScore,\n        pValue,\n      };\n\n      db.Trending.create(trendingData);\n    }\n  } catch (e) {\n    logger.error('Error processing trending content:', e);\n  }\n};\n"
  },
  {
    "path": "server/utils/videoProcessing.js",
    "content": "const getVideoDimensions = require('get-video-dimensions');\n\nasync function getVideoHeightAndWidth (filePath) {\n  const videoDimensions = await getVideoDimensions(filePath);\n  const { height, width } = videoDimensions;\n  return [ height, width ];\n}\n\nmodule.exports = {\n  getVideoHeightAndWidth,\n};\n"
  },
  {
    "path": "server/views/embed.handlebars",
    "content": "<div class=\"container\">\n  {{> logo}}\n  <video controls>\n    <source src=\"{{host}}/{{claimId}}/{{name}}.mp4\" type=\"video/mp4\">\n    Your browser does not support video\n  </video>\n</div>\n"
  },
  {
    "path": "server/views/layouts/embed.handlebars",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <style type=\"text/css\">\n  body {\n    margin: 0;\n    overflow: hidden;\n  }\n\n  .container {\n    height: 100vh;\n  }\n\n  video {\n    width: 100%;\n    height: 100%;\n  }\n\n  .logoLink {\n    margin-bottom: 80px; /* don't cover controls */\n    padding: 5px;\n    position: absolute;\n    z-index: 1;\n  }\n\n  .logo {\n    max-height: 128px;\n    max-width: 64px;\n  }\n\n  .bottom { bottom: 0 }\n  .right { right: 0 }\n  .padSmall { padding: 10px }\n  .padMedium { padding: 20px }\n  .padLarge { padding: 30px }\n  </style>\n</head>\n<body>\n  {{{ body }}}\n</body>\n</html>\n"
  },
  {
    "path": "server/views/partials/logo.handlebars",
    "content": "{{#if logoConfig}}\n  <a class=\"{{logoConfig.classNames}}\" href=\"{{#if logoConfig.logoLink}}{{logoConfig.logoLink}}{{else}}{{host}}/{{claimId}}/{{name}}{{/if}}\">\n    <img class=\"logo\" src=\"{{logoConfig.logoUrl}}\" />\n  </a>\n{{/if}}\n"
  },
  {
    "path": "server.js",
    "content": "const checkForLocalConfig = require('./utils/checkForLocalConfig.js');\n\ntry {\n  checkForLocalConfig('lbryConfig');\n  checkForLocalConfig('loggerConfig');\n  checkForLocalConfig('slackConfig');\n  checkForLocalConfig('mysqlConfig');\n  checkForLocalConfig('siteConfig');\n} catch (error) {\n  console.log(error);\n  process.exit(1);\n}\n\nlet currentApp;\n\ntry {\n  const Server = require('./server/');\n  currentApp = Server;\n\n  const speech = new Server();\n  speech.start();\n\n} catch (error) {\n  console.log('server startup error:', error);\n  process.exit(1);\n}\n\n/*\nTODO: Finish SSR HMR\nif (module.hot) {\n module.hot.accept('./server', () => {\n  server.removeListener('request', currentApp);\n  server.on('request', app);\n  currentApp = app;\n })\n}\n*/\n"
  },
  {
    "path": "test/end-to-end/end-to-end.test.js",
    "content": "require('../module-alias-boilerplate.js');\n\nconst chai = require('chai');\nconst expect = chai.expect;\nconst chaiHttp = require('chai-http');\nconst { details: { host } } = require('@config/siteConfig');\nconst { testChannel, testChannelId, testChannelPassword } = require('@devConfig/testingConfig.js');\nconst requestTimeout = 20000;\nconst publishTimeout = 120000;\nconst fs = require('fs');\n\nchai.use(chaiHttp);\n\nfunction testFor200StatusResponse (host, url) {\n  return it(`should receive a status code 200 within ${requestTimeout}ms`, function (done) {\n    chai.request(host)\n      .get(url)\n      .end(function (err, res) {\n        expect(err).to.be.null;\n        expect(res).to.have.status(200);\n        done();\n      });\n  }).timeout(requestTimeout);\n}\n\nfunction testShowRequestFor200StatusResponse (host, url) {\n  return it(`should receive a status code 200 within ${requestTimeout}ms`, function (done) {\n    chai.request(host)\n      .get(url)\n      .set('accept', 'text/html')\n      .end(function (err, res) {\n        expect(err).to.be.null;\n        expect(res).to.have.status(200);\n        done();\n      });\n  }).timeout(requestTimeout);\n}\n\ndescribe('end-to-end', function () {\n  describe('serve requests not from browser', function () {\n    const claimUrl = '/doitlive.jpg';\n    const claimUrlWithShortClaimId = '/d/doitlive.jpg';\n    const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg';\n\n    describe(claimUrl, function () {\n      testFor200StatusResponse(host, claimUrl);\n    });\n    describe(claimUrlWithShortClaimId, function () {\n      testFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n    describe(claimUrlWithLongClaimId, function () {\n      testFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n  });\n\n  describe('show requests from browser', function () {\n    const claimUrl = '/doitlive';\n    const claimUrlWithShortClaimId = '/d/doitlive';\n    const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive';\n\n    describe(claimUrl, function () {\n      testShowRequestFor200StatusResponse(host, claimUrl);\n    });\n    describe(claimUrlWithShortClaimId, function () {\n      testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n    describe(claimUrlWithLongClaimId, function () {\n      testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n  });\n\n  describe('serve requests browser (show lite)', function () {\n    const claimUrl = '/doitlive.jpg';\n    const claimUrlWithShortClaimId = '/d/doitlive.jpg';\n    const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg';\n\n    describe(claimUrl, function () {\n      testShowRequestFor200StatusResponse(host, claimUrl);\n    });\n    describe(claimUrlWithShortClaimId, function () {\n      testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n    describe(claimUrlWithLongClaimId, function () {\n      testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId);\n    });\n  });\n\n  describe('channel data request from client', function () {\n    const url = '/@test';\n    const urlWithShortClaimId = '/@test:3';\n    const urlWithMediumClaimId = '/@test:3b5bc6b6819172c6';\n    const urlWithLongClaimId = '/@test:3b5bc6b6819172c6e2f3f90aa855b14a956b4a82';\n\n    describe(url, function () {\n      it('should pass the tests I write here');\n    });\n    describe(urlWithShortClaimId, function () {\n      it('should pass the tests I write here');\n    });\n    describe(urlWithMediumClaimId, function () {\n      it('should pass the tests I write here');\n    });\n    describe(urlWithLongClaimId, function () {\n      it('should pass the tests I write here');\n    });\n  });\n\n  describe('publish requests', function () {\n    const publishUrl = '/api/claim/publish';\n    const filePath = './test/mock-data/bird.jpeg';\n    const fileName = 'byrd.jpeg';\n    const channelName = testChannel;\n    const channelId = testChannelId;\n    const channelPassword = testChannelPassword;\n    const date = new Date();\n    const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`;\n\n    describe('api/claim/publish', function () {\n\n      it(`should receive a status code 400 if username does not exist`, function (done) {\n        chai.request(host)\n          .post(publishUrl)\n          .type('form')\n          .attach('file', fs.readFileSync(filePath), fileName)\n          .field('name', name)\n          .field('channelName', `@${name}`)\n          .field('channelPassword', channelPassword)\n          .end(function (err, res) {\n            expect(res).to.have.status(400);\n            done();\n          });\n      }).timeout(publishTimeout);\n\n      it(`should receive a status code 400 if the wrong password is used with the channel name`, function (done) {\n        chai.request(host)\n          .post(publishUrl)\n          .type('form')\n          .attach('file', fs.readFileSync(filePath), fileName)\n          .field('name', name)\n          .field('channelName', channelName)\n          .field('channelPassword', 'xxxxx')\n          .end(function (err, res) {\n            expect(res).to.have.status(400);\n            done();\n          });\n      }).timeout(publishTimeout);\n\n      it(`should receive a status code 400 if the wrong password is used with the channel id`, function (done) {\n        chai.request(host)\n          .post(publishUrl)\n          .type('form')\n          .attach('file', fs.readFileSync(filePath), fileName)\n          .field('name', name)\n          .field('channelName', channelName)\n          .field('channelPassword', 'xxxxx')\n          .end(function (err, res) {\n            expect(res).to.have.status(400);\n            done();\n          });\n      }).timeout(publishTimeout);\n    });\n\n    describe('anonymous publishes', function () {\n      it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) {\n        chai.request(host)\n          .post(publishUrl)\n          .type('form')\n          .attach('file', fs.readFileSync(filePath), fileName)\n          .field('name', `${name}-anonymous`)\n          .end(function (err, res) {\n            expect(res).to.have.status(200);\n            done();\n          });\n      }).timeout(publishTimeout);\n    });\n\n    describe('in-channel publishes', function () {\n      it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) {\n        chai.request(host)\n          .post(publishUrl)\n          .type('form')\n          .attach('file', fs.readFileSync(filePath), fileName)\n          .field('name', `${name}-channel`)\n          .field('channelName', channelName)\n          .field('channelPassword', channelPassword)\n          .end(function (err, res) {\n            expect(res).to.have.status(200);\n            done();\n          });\n      }).timeout(publishTimeout);\n    });\n\n  });\n\n\n});\n"
  },
  {
    "path": "test/module-alias-boilerplate.js",
    "content": "// set up aliases\nconst moduleAlias = require('module-alias');\nconst customAliases = require('../utils/createModuleAliases.js')();\nmoduleAlias.addAliases(customAliases);\n"
  },
  {
    "path": "utils/checkForLocalConfig.js",
    "content": "module.exports = (name) => {\n  const config = require(`@config/${name}`);\n  if (!config) {\n    throw new Error(`No config file found for ${name}.  Please run 'npm run configure' to build your config files.`);\n  }\n};\n"
  },
  {
    "path": "utils/createCanonicalLink.js",
    "content": "const createBasicCanonicalLink = (page) => {\n  return `/${page}`;\n};\n\nconst createAssetCanonicalLink = (asset) => {\n  const { channelName, channelShortId, name, claimId, shortId } = asset;\n  return channelName ? `/${channelName}:${channelShortId}/${name}` : `/${shortId || claimId}/${name}`;\n};\n\nconst createChannelCanonicalLink = (channel) => {\n  const { name, shortId } = channel;\n  return `/${name}:${shortId}`;\n};\n\nconst createCanonicalLink = ({asset, channel, page}) => {\n  if (asset) {\n    return createAssetCanonicalLink(asset);\n  }\n  if (channel) {\n    return createChannelCanonicalLink(channel);\n  }\n  return createBasicCanonicalLink(page);\n};\n\nmodule.exports = createCanonicalLink;\n"
  },
  {
    "path": "utils/createModuleAliases.js",
    "content": "const { statSync, existsSync, readdirSync } = require('fs');\nconst { join, resolve } = require('path');\nconst DEFAULT_ROOT = 'client/src';\nconst CUSTOM_ROOT = 'site/custom/src';\nconst DEFAULT_SCSS_ROOT = 'client/scss';\nconst CUSTOM_SCSS_ROOT = 'site/custom/scss';\n\nconst getFolders = path => {\n  if (existsSync(path)) {\n    return readdirSync(path).filter(file => statSync(join(path, file)).isDirectory());\n  }\n  return [];\n};\n\nconst addAliasesForCustomComponentFolder = (name, aliasObject) => {\n  // creates an alias for each component in the folder that is passed to this function\n  const folderPath = resolve(`${CUSTOM_ROOT}/${name}`);\n  const components = getFolders(folderPath);\n  for (let i = 0; i < components.length; i++) {\n    let folderName = components[i];\n    let aliasName = `@${name}/${folderName}`;\n    aliasObject[aliasName] = resolve(`${CUSTOM_ROOT}/${name}/${folderName}/`);\n  }\n  return aliasObject;\n};\n\nmodule.exports = () => {\n  let moduleAliases = {};\n\n  moduleAliases['chainquery'] = resolve('./server/chainquery');\n  moduleAliases['server'] = resolve('./server');\n\n  // aliases for configs\n  moduleAliases['@config'] = resolve('site/config');\n  moduleAliases['@private'] = resolve('site/private');\n\n  // aliases for utils\n  moduleAliases['@globalutils'] = resolve('utils');\n  moduleAliases['@clientutils'] = resolve(`${DEFAULT_ROOT}/utils`);\n  // moduleAliases['@serverutils'] = resolve('server/utils');\n\n  // aliases for constants\n  moduleAliases['@clientConstants'] = resolve(`${DEFAULT_ROOT}/constants`);\n\n  // create specific aliases for locally defined components in the following folders\n  moduleAliases = addAliasesForCustomComponentFolder('containers', moduleAliases);\n  moduleAliases = addAliasesForCustomComponentFolder('components', moduleAliases);\n  moduleAliases = addAliasesForCustomComponentFolder('pages', moduleAliases);\n\n  // default component aliases\n  moduleAliases['@containers'] = resolve(`${DEFAULT_ROOT}/containers`);\n  moduleAliases['@components'] = resolve(`${DEFAULT_ROOT}/components`);\n  moduleAliases['@pages'] = resolve(`${DEFAULT_ROOT}/pages`);\n  moduleAliases['@actions'] = resolve(`${DEFAULT_ROOT}/actions`);\n  moduleAliases['@reducers'] = resolve(`${DEFAULT_ROOT}/reducers`);\n  moduleAliases['@sagas'] = resolve(`${DEFAULT_ROOT}/sagas`);\n  moduleAliases['@app'] = resolve(`${DEFAULT_ROOT}/app.js`);\n\n  // return finished aliases\n  return moduleAliases;\n};\n"
  },
  {
    "path": "utils/isApprovedChannel.js",
    "content": "function isApprovedChannel (channel, channels) {\n  const { name, shortId: short, longId: long } = channel;\n  return Boolean(\n    (long && channels.find(chan => chan.longId === long)) ||\n    (name && short && channels.find(chan => chan.name === name && chan.shortId === short))\n  );\n}\n\nmodule.exports = isApprovedChannel;\n"
  },
  {
    "path": "utils/lbryUri.js",
    "content": "module.exports = {\n  REGEXP_INVALID_CLAIM  : /[^A-Za-z0-9-]/g,\n  REGEXP_INVALID_CHANNEL: /[^A-Za-z0-9-@]/g,\n  REGEXP_ADDRESS        : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/,\n  CHANNEL_CHAR          : '@',\n  parseIdentifier       : function (identifier) {\n    const componentsRegex = new RegExp(\n      '([^:$#/]*)' + // value (stops at the first separator or end)\n      '([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)\n    );\n    const [, value, modifierSeperator, modifier] = componentsRegex\n      .exec(identifier)\n      .map(match => match || null);\n\n    // Validate and process name\n    if (!value) {\n      throw new Error(`Check your url.  No channel name provided before \"${modifierSeperator}\"`);\n    }\n    const isChannel = value.startsWith(module.exports.CHANNEL_CHAR);\n    const channelName = isChannel ? value : null;\n    let claimId;\n    if (isChannel) {\n      if (!channelName) {\n        throw new Error('No channel name after @.');\n      }\n      const nameBadChars = (channelName).match(module.exports.REGEXP_INVALID_CHANNEL);\n      if (nameBadChars) {\n        throw new Error(`Invalid characters in channel name: ${nameBadChars.join(', ')}.`);\n      }\n    } else {\n      claimId = value;\n    }\n\n    // Validate and process modifier\n    let channelClaimId;\n    if (modifierSeperator) {\n      if (!modifier) {\n        throw new Error(`No modifier provided after separator \"${modifierSeperator}\"`);\n      }\n\n      if (modifierSeperator === ':') {\n        channelClaimId = modifier;\n      } else {\n        throw new Error(`The \"${modifierSeperator}\" modifier is not currently supported`);\n      }\n    }\n    return {\n      isChannel,\n      channelName,\n      channelClaimId,\n      claimId,\n    };\n  },\n  parseClaim: function (claim) {\n    const componentsRegex = new RegExp(\n      '([^:$#/.]*)' + // name (stops at the first modifier)\n      '([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)\n    );\n    const [, claimName, modifierSeperator, modifier] = componentsRegex\n      .exec(claim)\n      .map(match => match || null);\n\n    // Validate and process name\n    if (!claimName) {\n      throw new Error('No claim name provided before .');\n    }\n    const nameBadChars = (claimName).match(module.exports.REGEXP_INVALID_CLAIM);\n    if (nameBadChars) {\n      throw new Error(`Invalid characters in claim name: ${nameBadChars.join(', ')}.`);\n    }\n    // Validate and process modifier\n    if (modifierSeperator) {\n      if (!modifier) {\n        throw new Error(`No file extension provided after separator ${modifierSeperator}.`);\n      }\n      if (modifierSeperator !== '.') {\n        throw new Error(`The ${modifierSeperator} modifier is not supported in the claim name`);\n      }\n    }\n    // return results\n    return {\n      claimName,\n      extension: modifier || null,\n    };\n  },\n  parseModifier: function (claim) {\n    const componentsRegex = new RegExp(\n      '([^:$#/.]*)' + // name (stops at the first modifier)\n      '([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)\n    );\n    const [ , , modifierSeperator ] = componentsRegex\n      .exec(claim)\n      .map(match => match || null);\n\n    // Validate and process modifier\n    let hasFileExtension = false;\n    if (modifierSeperator) {\n      hasFileExtension = true;\n    }\n    return {\n      hasFileExtension,\n    };\n  },\n};\n"
  },
  {
    "path": "utils/validateFileForPublish.js",
    "content": "import { publishing } from '@config/siteConfig.json';\n\nconst { fileSizeLimits } = publishing;\n\nconst SIZE_MB = 1000000;\n\nexport default function validateFileForPublish(file) {\n  let contentType = file.type;\n  let mediaType = contentType ? contentType.substr(0, contentType.indexOf('/')) : '';\n  let mediaTypeLimit = fileSizeLimits[mediaType] || false;\n  let customLimits = fileSizeLimits['customByContentType'];\n\n  if (!file) {\n    throw new Error('no file provided');\n  }\n\n  if (/'/.test(file.name)) {\n    throw new Error('apostrophes are not allowed in the file name');\n  }\n\n  if (Object.keys(customLimits).includes(contentType)) {\n    if (file.size > customLimits[contentType]) {\n      throw new Error(\n        `Sorry, type ${contentType} is limited to ${customLimits[contentType] / SIZE_MB} MB.`\n      );\n    }\n  }\n  if (mediaTypeLimit) {\n    if (file.size > mediaTypeLimit) {\n      throw new Error(`Sorry, type ${mediaType} is limited to ${mediaTypeLimit / SIZE_MB} MB.`);\n    }\n  }\n  return file;\n}\n"
  },
  {
    "path": "webpack/webpack.client.config.js",
    "content": "const Path = require('path');\nconst webpack = require('webpack');\nconst nodeExternals = require('webpack-node-externals');\nconst ExtractCssChunks = require('extract-css-chunks-webpack-plugin');\nconst createModuleAliases = require('../utils/createModuleAliases.js');\n\nconst SCSS_ROOT = Path.resolve(__dirname, '../client/scss/');\nconst CLIENT_ROOT = Path.resolve(__dirname, '../client/');\nconst CUSTOM_CLIENT_ROOT = Path.resolve(__dirname, '../site/custom/');\n\nconst customAliases = createModuleAliases();\n\nmodule.exports = (env, argv) => {\n  const isDev = argv.mode === 'development';\n\n  return {\n    mode: isDev ? 'development' : 'production',\n    target: 'web',\n    entry : [\n      ...(isDev ? ['webpack-hot-middleware/client'] : []),\n      //'webpack/hot/dev-server',\n      '@babel/polyfill',\n      'whatwg-fetch',\n      './client/src/index.js',\n    ],\n    output: {\n      path      : Path.resolve(__dirname, '../public/bundle'),\n      publicPath: '/bundle/',\n      filename  : 'bundle.js',\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.jsx?$/,\n          exclude: /(node_modules|bower_components)/,\n          use: {\n            loader: 'babel-loader',\n          },\n        },\n        {\n          test: /\\.scss$/,\n          use: [\n            {\n              loader: ExtractCssChunks.loader,\n            },\n            'css-loader',\n            'sass-loader',\n          ]\n        },\n        {\n          test: /\\.(png|jpg|gif|otf|ttf|svg)$/,\n          use : [\n            {\n              loader : 'url-loader',\n              options: {\n                limit: 8192,\n                name : '[name]-[hash].[ext]',\n              },\n            },\n          ],\n        },\n      ],\n    },\n    resolve: {\n      modules: [\n        CUSTOM_CLIENT_ROOT,\n        CLIENT_ROOT,\n        SCSS_ROOT,\n        'node_modules',\n        __dirname,\n      ],\n      alias     : customAliases,\n      extensions: ['.js', '.jsx', '.scss', '.json'],\n    },\n    plugins: [\n      ...(isDev ? [new webpack.HotModuleReplacementPlugin()] : []),\n      new ExtractCssChunks({\n        filename: 'style.css', // '[name].css',\n      })\n    ],\n  };\n}\n"
  },
  {
    "path": "webpack/webpack.server.config.js",
    "content": "const Path = require('path');\nconst webpack = require('webpack');\nconst nodeExternals = require('webpack-node-externals');\nconst ExtractCssChunks = require('extract-css-chunks-webpack-plugin');\nconst createModuleAliases = require('../utils/createModuleAliases.js');\n\nconst SCSS_ROOT = Path.resolve(__dirname, '../client/scss/');\nconst CLIENT_ROOT = Path.resolve(__dirname, '../client/');\nconst CUSTOM_CLIENT_ROOT = Path.resolve(__dirname, '../site/custom/');\n\nconst customAliases = createModuleAliases();\n\nmodule.exports = (env, argv) => {\n  const isDev = argv.mode === 'development';\n\n  return {\n    target: 'node',\n    //watch: isDev,\n    externals: [nodeExternals({\n      whitelist: ['webpack/hot/poll?1000'],\n    })],\n\n    // Set __dirname relative to current __dirname for node\n    context: Path.resolve(__dirname, '../'),\n    node: {\n      __dirname: true,\n    },\n\n    entry : [\n      ...(isDev ? ['webpack/hot/poll?1000'] : []),\n      '@babel/polyfill',\n      './server.js'\n    ],\n    output: {\n      path      : Path.resolve(__dirname, '../server/bundle'),\n      //publicPath: '/bundle/',\n      filename  : 'server.js',\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.jsx?$/,\n          exclude: /(node_modules|bower_components)/,\n          use: {\n            loader: 'babel-loader',\n          },\n        },\n        {\n          test: /\\.scss$/,\n          use: [\n            {\n              loader: ExtractCssChunks.loader,\n            },\n            'css-loader',\n            'sass-loader',\n          ]\n        },\n        {\n          test: /\\.(png|jpg|gif|otf|ttf|svg)$/,\n          use : [\n            {\n              loader : 'url-loader',\n              options: {\n                limit: 8192,\n                name : '[name]-[hash].[ext]',\n              },\n            },\n          ],\n        },\n      ],\n    },\n    resolve: {\n      modules: [\n        CUSTOM_CLIENT_ROOT,\n        CLIENT_ROOT,\n        SCSS_ROOT,\n        'node_modules',\n        __dirname,\n      ],\n      alias     : customAliases,\n      extensions: ['.js', '.jsx', '.scss', '.json'],\n    },\n    plugins: [\n      new webpack.HotModuleReplacementPlugin(),\n    ]\n  };\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "module.exports = (env, argv) => {\n  const isDev = argv.mode === 'development';\n\n  return isDev ? [\n    require('./webpack/webpack.server.config')(env, argv),\n  ] : [\n    require('./webpack/webpack.server.config')(env, argv),\n    require('./webpack/webpack.client.config')(env, argv),\n  ];\n}\n"
  }
]