├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── clean_all.sh ├── clean_content.sh ├── clean_logs.sh ├── clean_pids.sh ├── client ├── img │ ├── attachment.jpg │ ├── banner.jpg │ ├── fade-blue.png │ └── favicon.ico ├── inc │ ├── controller.js │ ├── functions.js │ ├── page.js │ ├── pagehtml.js │ ├── post.js │ └── serverbridge.js ├── index.html ├── smugchan.js └── styles.css ├── common ├── inputhandler.js ├── package.json ├── payloads.js ├── settings.js ├── sio.js ├── slog.js ├── storage.js ├── storagebrowser.js ├── util.js └── wrapperfuncs.js ├── install_all.sh ├── kill_all.sh ├── nameserver ├── namemanager.js └── package.json ├── populate_boards.sh ├── scripts ├── addboardtosite.js ├── addthreadtoboard.js ├── createboard.js ├── createmod.js ├── createserver.js ├── createsite.js └── createthread.js ├── server ├── package.json └── server.js ├── setup.sh └── start_all.sh /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ], 28 | "eqeqeq": [ 29 | "warn", 30 | "smart" 31 | ], 32 | "no-console": [ 33 | "off" 34 | ], 35 | "brace-style": [ 36 | "error", 37 | "1tbs" 38 | ], 39 | "curly": [ 40 | "error", 41 | "all" 42 | ] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # ipfs repo 61 | .ipfs/ 62 | 63 | # logs 64 | nameserver/log.txt 65 | server/log.txt 66 | 67 | # locks 68 | common/package-lock.json 69 | nameserver/package-lock.json 70 | server/package-lock.json 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 smugdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smugboard 2 | An IPFS-based imageboard. 3 | 4 | ## Prerequisites 5 | * Node 6 | 7 | * Browserify 8 | 9 | * go-ipfs 10 | 11 | * eslint (if you want to contribute) 12 | 13 | ## Setup 14 | ``` 15 | ./install_all.sh 16 | ./start_all.sh 17 | ./setup.sh 18 | ``` 19 | 20 | ## Usage 21 | 22 | The site can be accessed from any local go-ipfs daemon, provided that it is configured with 23 | ``` 24 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]" 25 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\"true\"]" 26 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\"PUT\", \"POST\", \"GET\"]" 27 | ``` 28 | beforehand. Then, simply visit the link as output by setup.sh. Make sure to restart the daemon after updating the configuration. 29 | -------------------------------------------------------------------------------- /clean_all.sh: -------------------------------------------------------------------------------- 1 | rm -rf .ipfs 2 | rm -rf common/node_modules 3 | rm -rf nameserver/node_modules 4 | rm -rf server/node_modules 5 | 6 | ./clean_content.sh 7 | ./clean_logs.sh 8 | ./clean_pids.sh 9 | -------------------------------------------------------------------------------- /clean_content.sh: -------------------------------------------------------------------------------- 1 | rm -f nameserver/bindings.json 2 | rm -f client/bundle.js 3 | rm -f server/wrappers.json 4 | -------------------------------------------------------------------------------- /clean_logs.sh: -------------------------------------------------------------------------------- 1 | rm -f server/log.txt 2 | rm -f nameserver/log.txt 3 | 4 | -------------------------------------------------------------------------------- /clean_pids.sh: -------------------------------------------------------------------------------- 1 | rm -f .ipfs/ipfs.pid 2 | rm -f server/server.pid 3 | rm -f nameserver/server.pid 4 | -------------------------------------------------------------------------------- /client/img/attachment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smugdev/smugboard/16fb615c0c4aee435dfd2d0c8df77e740501a26d/client/img/attachment.jpg -------------------------------------------------------------------------------- /client/img/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smugdev/smugboard/16fb615c0c4aee435dfd2d0c8df77e740501a26d/client/img/banner.jpg -------------------------------------------------------------------------------- /client/img/fade-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smugdev/smugboard/16fb615c0c4aee435dfd2d0c8df77e740501a26d/client/img/fade-blue.png -------------------------------------------------------------------------------- /client/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smugdev/smugboard/16fb615c0c4aee435dfd2d0c8df77e740501a26d/client/img/favicon.ico -------------------------------------------------------------------------------- /client/inc/controller.js: -------------------------------------------------------------------------------- 1 | var slog = require('../../common/slog.js'); 2 | var page = require('./page.js'); 3 | var functions = require('./functions.js'); 4 | var wrapperFuncs = require('../../common/wrapperfuncs.js'); 5 | //var handler = require('../../common/inputhandler.js'); 6 | 7 | var secondsLeft = 30; 8 | var pendingRefresh = false; 9 | var refreshTimeout = null; 10 | 11 | //after .last load 12 | function mergeToClientInitial(wrapper){ 13 | if (wrapper.last.data.info.mode === 'site'){ 14 | //handle title bar 15 | //TODO 16 | } else if (wrapper.last.data.info.mode === 'board'){ 17 | //handle the title bar and stuff, handle banners 18 | } else if (wrapper.last.data.info.mode === 'thread'){ 19 | //do nothing(?) 20 | } 21 | } 22 | 23 | //after sLog load 24 | function mergeToClientMidpoint(wrapper, addressObj){ 25 | if (wrapper.last.data.info.mode === 'site'){ 26 | //handle board list 27 | let sitePublicAddress; 28 | for (let address of addressObj) { 29 | if (address.mode === 'site') { 30 | sitePublicAddress = address.publicAddress; 31 | } 32 | } 33 | page.fillBoardlist(sitePublicAddress, wrapper.sLog); 34 | } else if (wrapper.last.data.info.mode === 'board'){ 35 | //do nothing(?) 36 | } else if (wrapper.last.data.info.mode === 'thread'){ 37 | //do nothing(?) 38 | } 39 | } 40 | 41 | //everything is loaded, and this is the endpoint 42 | function mergeToClientEndpoint(id, wrappers, knownMods, addressObj, skipMerges){ 43 | if (!skipMerges){ 44 | if (wrappers[id].last.data.info.mode === 'site'){ 45 | //display site 46 | page.setSiteHTML(addressObj); 47 | //page.setNavLinks(false, addressObj); 48 | } else if (wrappers[id].last.data.info.mode === 'board'){ 49 | //display board, or catalog if addressParsed[addressParsed.length - 1].mode === 'catalog' 50 | if (addressObj[addressObj.length - 1].mode === 'catalog'){ 51 | page.setCatalogHTML(addressObj); 52 | } else { 53 | page.setBoardHTML(addressObj); 54 | } 55 | page.setPostFormData(id); 56 | page.setNavLinks(false, addressObj); 57 | page.setHeader(wrappers, addressObj); 58 | } else if (wrappers[id].last.data.info.mode === 'thread'){ 59 | //display thread 60 | page.setThreadHTML(); 61 | page.setNavLinks(true, addressObj); 62 | page.setHeader(wrappers, addressObj); 63 | page.setPostFormData(id); 64 | } 65 | } 66 | 67 | if (wrappers[id].last.data.info.mode === 'site'){ 68 | //display site 69 | let sitePublicAddress; 70 | for (let address of addressObj) { 71 | if (address.mode === 'site') { 72 | sitePublicAddress = address.publicAddress; 73 | } 74 | } 75 | return page.fillBoardlistTable(sitePublicAddress, id, wrappers); 76 | } else if (wrappers[id].last.data.info.mode === 'board'){ 77 | //display board, or catalog if addressParsed[addressParsed.length - 1].mode === 'catalog' 78 | if (addressObj[addressObj.length - 1].mode === 'catalog'){ 79 | return page.loadCatalog(id, wrappers, knownMods); 80 | } else { 81 | return page.loadBoard(id, wrappers, addressObj, knownMods); 82 | } 83 | } else if (wrappers[id].last.data.info.mode === 'thread'){ 84 | //display thread 85 | return page.loadThread(null, id, wrappers, addressObj, knownMods, false, false).then(() => { 86 | refreshThread(id)(); 87 | document.getElementById('update_thread').setAttribute('onclick', 'window.smug.forceRefresh("' + id + '")'); 88 | if (addressObj[addressObj.length - 1].mode === 'post'){ 89 | //TODO scroll to post 90 | } 91 | return; 92 | }); 93 | } 94 | } 95 | 96 | function handleWrapperRecursive(addressObj, wrappers, index, skipMerges, knownMods, modsPending){ 97 | if (modsPending == null) { 98 | modsPending = []; 99 | } 100 | if (knownMods == null) { 101 | knownMods = []; 102 | } 103 | let currentId = addressObj[index].actualAddress; 104 | let endpoint = false; 105 | 106 | return wrapperFuncs.pullWrapperStub(currentId, wrappers).then(() => { 107 | modsPending = modsPending.concat(wrapperFuncs.collectMods(wrappers[currentId].last.data.info.mods, wrappers)); 108 | if (wrappers[currentId].last.data.info.mods != null) { 109 | knownMods = knownMods.concat(wrappers[currentId].last.data.info.mods); 110 | } 111 | addressObj[index].mode = wrappers[currentId].last.data.info.mode; 112 | if (!skipMerges) { 113 | mergeToClientInitial(wrappers[currentId]); 114 | } 115 | 116 | if (index === addressObj.length - 1){ 117 | //last item 118 | endpoint = true; 119 | } else if (wrappers[currentId].last.data.info.mode === 'board' && addressObj.length - 1 === index + 1 && addressObj[index + 1].publicAddress === 'catalog'){ 120 | //catalog designator 121 | addressObj[index + 1].mode = 'catalog'; 122 | endpoint = true; 123 | } else if (wrappers[currentId].last.data.info.mode === 'thread' && addressObj.length - 1 === index + 1){ 124 | //next item is just the post # 125 | addressObj[index + 1].mode = 'post'; 126 | endpoint = true; 127 | } 128 | 129 | let promises = []; 130 | promises.push( 131 | wrapperFuncs.loadWrapper(wrappers[currentId]).then(() => { 132 | return slog.calculateSLogPayloads(wrappers[currentId]); 133 | }) 134 | ); 135 | if (endpoint){ 136 | promises = promises.concat(modsPending); 137 | } 138 | return Promise.all(promises); 139 | 140 | }).then(() => { 141 | if (!skipMerges) { 142 | mergeToClientMidpoint(wrappers[currentId], addressObj); 143 | } 144 | 145 | if (endpoint){ 146 | return mergeToClientEndpoint(currentId, wrappers, knownMods, addressObj, skipMerges); 147 | } else { 148 | //something else left, continue recursing 149 | addressObj[index + 1].actualAddress = functions.findAssociation(addressObj[index + 1].publicAddress, wrappers[currentId].sLog); 150 | if (addressObj[index + 1].actualAddress == null) { 151 | return Promise.reject('Item not found: ' + addressObj[index + 1].publicAddress); 152 | } else { 153 | return handleWrapperRecursive(addressObj, wrappers, index + 1, skipMerges, knownMods, modsPending); 154 | } 155 | } 156 | }); 157 | //.catch(console.error); 158 | } 159 | 160 | 161 | /*function refresh(id, knownMods, wrappers, addressObj){ 162 | //Promise.all for wrappers[id] (use wrapperFuncs.pullWrapperStub -> loadWrapper) and for each mod of knownMods (use wrapperFuncs.collectMods) 163 | 164 | 165 | let modsPending = wrapperFuncs.collectMods(knownMods, wrappers); 166 | let threadId = null; 167 | for (let item of addressObj){ 168 | if (item.mode === 'thread'){ 169 | threadId = item.actualAddress; 170 | } 171 | } 172 | 173 | let promises = modsPending.concat(); 174 | 175 | Promise.all(modsPending) 176 | //page.loadThread(null, threadId, wrappers, addressObj, knownMods, modMode, fromIndex, indexSeqno) 177 | }*/ 178 | 179 | function forceRefresh(id){ 180 | pendingRefresh = true; 181 | refreshThread(id)(); 182 | } 183 | 184 | function queueRefresh(){ 185 | secondsLeft = 5; 186 | } 187 | 188 | //function refreshThread(id, wrappers, knownMods, addressObj){ 189 | function refreshThread(id){ 190 | /*let refreshData = {}; 191 | refreshData.id = id; 192 | refreshData.wrappers = wrappers;//use global? 193 | refreshData.knownMods = knownMods; 194 | refreshData.addressObj = addressObj; 195 | 196 | var smug = window.smug || {}; 197 | smug.refreshData = refreshData;*/ 198 | 199 | 200 | haltRefresh(); 201 | let index = 0; 202 | for (let i = 0; i < window.smug.addressObj.length; i++){ 203 | if (window.smug.addressObj[i].mode === 'thread'){ 204 | index = i; 205 | } 206 | } 207 | 208 | return () => { 209 | if (secondsLeft === 0 || pendingRefresh){ 210 | handleWrapperRecursive(window.smug.addressObj, window.smug.wrappers, index, true, window.smug.knownMods); 211 | //handleWrapperRecursive(addressObj, wrappers, index, true, knownMods); 212 | //refresh(id, knownMods, wrappers, addressObj); 213 | document.getElementById('update_secs').innerHTML = 'Updating...'; 214 | secondsLeft = 30; 215 | pendingRefresh = false; 216 | } else { 217 | secondsLeft--; 218 | document.getElementById('update_secs').innerHTML = secondsLeft; 219 | refreshTimeout = window.setTimeout(refreshThread(id), 1000); 220 | } 221 | }; 222 | } 223 | 224 | function haltRefresh(){ 225 | clearTimeout(refreshTimeout); 226 | } 227 | 228 | module.exports.forceRefresh = forceRefresh; 229 | module.exports.queueRefresh = queueRefresh; 230 | //module.exports.refreshThread = refreshThread; 231 | module.exports.handleWrapperRecursive = handleWrapperRecursive; 232 | module.exports.haltRefresh = haltRefresh; 233 | -------------------------------------------------------------------------------- /client/inc/functions.js: -------------------------------------------------------------------------------- 1 | var util = require('../../common/util.js'); 2 | 3 | function parseLocationAddress(windowAddress){ 4 | windowAddress = windowAddress.replace(/^#/, ''); 5 | var addressInternals = []; 6 | 7 | var addressArr = windowAddress.split('/'); 8 | 9 | for (let i = 0; i < addressArr.length; i++){ 10 | if (addressArr[i] === ''){ 11 | //do nothing 12 | } else if (addressArr[i] === 'ipns' || addressArr[i] === 'ipfs'){ 13 | addressInternals.push({ 14 | actualAddress: '/' + addressArr[i] + '/' + addressArr[i+1], 15 | publicAddress: '/' + addressArr[i] + '/' + addressArr[i+1] 16 | }); 17 | i++; 18 | } else if (addressInternals.length === 0) { 19 | //assume IPNS 20 | addressInternals.push({ 21 | actualAddress: '/ipns/' + addressArr[i], 22 | publicAddress: addressArr[i] 23 | }); 24 | } else { 25 | addressInternals.push({ 26 | publicAddress: addressArr[i] 27 | }); 28 | } 29 | } 30 | 31 | return addressInternals; 32 | } 33 | 34 | function getNewHash(addressParsed, unwantedModes, extras){ 35 | if (unwantedModes == null) { 36 | unwantedModes = []; 37 | } 38 | 39 | var newHash = '#'; 40 | for (let item of addressParsed){ 41 | if (item.mode != null && unwantedModes.indexOf(item.mode) === -1){ 42 | if (newHash === '#'){ 43 | newHash += item.publicAddress; 44 | } else { 45 | newHash += '/' + item.publicAddress; 46 | } 47 | } 48 | } 49 | 50 | if (extras != null) { 51 | for (let item of extras) { 52 | newHash += '/' + item; 53 | } 54 | } 55 | 56 | return newHash; 57 | } 58 | 59 | function scrollToCurrentPost(){ 60 | let targetID = location.hash.replace(/^#/, ''); 61 | let target = document.getElementById(targetID); 62 | if (target != null) { 63 | document.body.scrollTop = target.offsetTop; 64 | } 65 | } 66 | 67 | function findAssociation(publicAddress, sLog){ 68 | for (let entry of sLog){ 69 | if (!entry.removed && util.propertyExists(entry, 'data.payload.data.thread.address') && entry.data.seqno){ 70 | //if (!entry.removed && entry.data && entry.data.payload && entry.data.seqno && entry.data.payload.data && entry.data.payload.data.thread && entry.data.payload.data.thread.address){ 71 | if (publicAddress === entry.data.seqno + ''){ 72 | return entry.data.payload.data.thread.address; 73 | } 74 | } 75 | if (!entry.removed && util.propertyExists(entry, 'data.payload.data.uri')){ 76 | //if (!entry.removed && entry.data && entry.data.payload && entry.data.payload.data && entry.data.payload.data.uri){ 77 | if (publicAddress === entry.data.payload.data.uri){ 78 | return entry.data.payload.data.address; 79 | } 80 | } 81 | } 82 | return null; 83 | } 84 | 85 | module.exports.parseLocationAddress = parseLocationAddress; 86 | module.exports.getNewHash = getNewHash; 87 | module.exports.scrollToCurrentPost = scrollToCurrentPost; 88 | module.exports.findAssociation = findAssociation; 89 | 90 | -------------------------------------------------------------------------------- /client/inc/page.js: -------------------------------------------------------------------------------- 1 | var pageHtml = require('./pagehtml.js'); 2 | var functions = require('./functions.js'); 3 | var wrapperFuncs = require('../../common/wrapperfuncs.js'); 4 | var slog = require('../../common/slog.js'); 5 | 6 | var browserDisplayableFormats = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico']; 7 | 8 | function decoratePostBody(bodyRaw, indexSeqno, addressObj){ 9 | let bodyPretty = bodyRaw + ''; 10 | 11 | bodyPretty = bodyPretty.replace(/&/g, '&'); 12 | bodyPretty = bodyPretty.replace(//g, '>'); 14 | bodyPretty = bodyPretty.replace(/(>.*)(\r|\n|$)/g, '$1$2'); 15 | 16 | //TODO strikethrough 17 | bodyPretty = bodyPretty.replace(/\*\*(.*)\*\*/g, '$1'); 18 | bodyPretty = bodyPretty.replace(/==(.*)==/g, '$1'); 19 | bodyPretty = bodyPretty.replace(/'''(.*)'''/g, '$1'); 20 | bodyPretty = bodyPretty.replace(/''(.*)''/g, '$1'); 21 | 22 | let modes = ['site', 'board', 'thread', 'post']; 23 | bodyPretty = bodyPretty.replace(/>>>>>\/([a-z0-9][a-z0-9]*)\/([a-z0-9][a-z0-9]*)\/(\d+)\/(\d+)/g, '>>>>>/$1/$2/$3/$4'); 24 | modes = ['board', 'thread', 'post']; 25 | bodyPretty = bodyPretty.replace(/>>>>\/([a-z0-9][a-z0-9]*)\/(\d+)\/(\d+)/g, '>>>>/$1/$2/$3'); 26 | modes = ['thread', 'post']; 27 | bodyPretty = bodyPretty.replace(/>>>\/(\d+)\/(\d+)/g, '>>>/$1/$2');//TODO i think you could break this by putting a '$' in the url hash 28 | modes = ['post']; 29 | bodyPretty = bodyPretty.replace(/>>(\d+)/g, '>>$1');//TODO remove class 30 | 31 | return bodyPretty; 32 | } 33 | 34 | function pad(n) { 35 | return (n < 10) ? ('0' + n) : n; 36 | } 37 | 38 | function fillPost(postDiv, post, seqno, timestamp, addressObj, viewingFromIndex, indexSeqno, mod, flagged){ 39 | //var post = JSON.parse(postJson); 40 | 41 | if (seqno === 1){ 42 | postDiv.setAttribute('class', 'post op'); 43 | } else { 44 | postDiv.setAttribute('class', 'post reply'); 45 | } 46 | 47 | if (flagged) { 48 | postDiv.setAttribute('class', postDiv.getAttribute('class') + ' flagged'); 49 | } 50 | 51 | let extras = []; 52 | if (indexSeqno) { 53 | extras.push(indexSeqno); 54 | } 55 | extras.push(seqno); 56 | let postFullLink = document.createElement('div'); 57 | postFullLink.setAttribute('id', functions.getNewHash(addressObj, ['post'], extras)); 58 | 59 | let postIntro = document.createElement('p'); 60 | postIntro.setAttribute('class', 'intro'); 61 | 62 | if (post.subject != null && post.subject !== ''){ 63 | let subjectSpan = document.createElement('span'); 64 | subjectSpan.setAttribute('class', 'subject'); 65 | subjectSpan.appendChild(document.createTextNode(post.subject)); 66 | postIntro.appendChild(subjectSpan); 67 | } else { 68 | let subjectSpan = document.createElement('span'); 69 | subjectSpan.setAttribute('class', 'blanksubject'); 70 | postIntro.appendChild(subjectSpan); 71 | } 72 | 73 | if (post.author != null){ 74 | let nameSpan; 75 | if (post.email != null && post.email !== ''){ 76 | nameSpan = document.createElement('a'); 77 | nameSpan.setAttribute('class', 'email'); 78 | nameSpan.setAttribute('href', 'mailto:' + post.email); 79 | } else { 80 | nameSpan = document.createElement('span'); 81 | nameSpan.setAttribute('class', 'name'); 82 | } 83 | nameSpan.appendChild(document.createTextNode(post.author)); 84 | postIntro.appendChild(nameSpan); 85 | } 86 | 87 | if (timestamp != null){ 88 | let date = new Date(timestamp); 89 | let timeString = date.getFullYear() + '-' + pad(date.getMonth()+1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); 90 | let dateSpan = document.createElement('span'); 91 | dateSpan.setAttribute('class', 'time'); 92 | dateSpan.appendChild(document.createTextNode(timeString)); 93 | postIntro.appendChild(dateSpan); 94 | } 95 | 96 | if (seqno != null){ 97 | let postNum = document.createElement('a'); 98 | postNum.setAttribute('id', '' + seqno); 99 | postNum.setAttribute('class', 'post_no'); 100 | postNum.setAttribute('href', functions.getNewHash(addressObj, ['post'], extras)); 101 | postNum.appendChild(document.createTextNode('No. ')); 102 | postIntro.appendChild(postNum); 103 | postIntro.appendChild(document.createTextNode(seqno)); 104 | } 105 | 106 | if (viewingFromIndex && seqno === 1 && indexSeqno != null){ 107 | let replyLink = document.createElement('a'); 108 | replyLink.setAttribute('class', 'reply_link'); 109 | replyLink.setAttribute('href', functions.getNewHash(addressObj, ['post', 'thread'], [indexSeqno]));//location.hash + '/' + indexSeqno);//postDiv.getAttribute('data-thread'));//TODO this should be able to handle detached threads 110 | replyLink.appendChild(document.createTextNode('[Reply]')); 111 | postIntro.appendChild(replyLink); 112 | } 113 | 114 | let postFiles = document.createElement('div'); 115 | if (post.files != null && post.files.length > 0){ 116 | postFiles.setAttribute('class', 'files'); 117 | for (let file of post.files){ 118 | if (file.address != null && file.size != null){ 119 | let fileDiv = document.createElement('div'); 120 | fileDiv.setAttribute('class', 'file'); 121 | let fileInfo = document.createElement('p'); 122 | fileInfo.setAttribute('class', 'fileinfo'); 123 | let fileSpan = document.createElement('span'); 124 | fileSpan.appendChild(document.createTextNode('File: ')); 125 | let fileLink = document.createElement('a'); 126 | fileLink.setAttribute('href', file.address); 127 | fileLink.appendChild(document.createTextNode(file.filename)); 128 | 129 | let fileDetails = document.createElement('span'); 130 | fileDetails.setAttribute('class', 'unimportant'); 131 | let fileDims = ''; 132 | if (file.width != null && file.height != null){ 133 | fileDims = ', ' + file.width + 'x' + file.height; 134 | } 135 | fileDetails.appendChild(document.createTextNode(' (' + (Math.round((parseFloat(file.size) / 1024.0) * 100)/100.0) + 'KiB' + fileDims + ')')); 136 | 137 | let fileImgLink = document.createElement('a'); 138 | fileImgLink.setAttribute('href', file.address); 139 | let fileImg = document.createElement('img'); 140 | fileImg.setAttribute('class', 'post-image'); 141 | 142 | if (file.extension != null && browserDisplayableFormats.indexOf(file.extension) !== -1 && file.height != null && file.width != null){ 143 | fileImg.setAttribute('src', file.address); 144 | 145 | let maxDim = 255; 146 | let imgWidth = parseInt(file.width); 147 | let imgHeight = parseInt(file.height); 148 | let longSide = imgHeight > imgWidth ? imgHeight : imgWidth; 149 | 150 | if (longSide > maxDim){ 151 | let factor = longSide / maxDim; 152 | imgHeight /= factor; 153 | imgWidth /= factor; 154 | } 155 | 156 | fileImg.setAttribute('style', 'width: ' + imgWidth + 'px; height: ' + imgHeight + 'px;'); 157 | } else { 158 | fileImg.setAttribute('src', '/ipfs/QmSmVimunQTsmau3SH9ohESmzRNLCdXGjSHYpxvp9SeZp2');//TODO shouldn't have direct IPFS hashes in here 159 | } 160 | 161 | 162 | fileSpan.appendChild(fileLink); 163 | fileSpan.appendChild(fileDetails); 164 | fileInfo.appendChild(fileSpan); 165 | fileImgLink.appendChild(fileImg); 166 | fileDiv.appendChild(fileInfo); 167 | fileDiv.appendChild(fileImgLink); 168 | 169 | postFiles.appendChild(fileDiv); 170 | } 171 | } 172 | } 173 | 174 | let modControls = document.createElement('span'); 175 | if (mod){ 176 | modControls.setAttribute('class', 'controls'); 177 | let modDelete = document.createElement('a'); 178 | modDelete.setAttribute('href', '#'); 179 | if (flagged){ 180 | modDelete.setAttribute('onclick', 'return smug.modOperation("undelete", "' + mod + '", "' + postDiv.getAttribute('data-thread') + '", "' + seqno + '", smug.refreshPage)'); 181 | modDelete.innerHTML = '[-D]'; 182 | } else { 183 | modDelete.setAttribute('onclick', 'return smug.modOperation("delete", "' + mod + '", "' + postDiv.getAttribute('data-thread') + '", "' + seqno + '", smug.refreshPage)'); 184 | modDelete.innerHTML = '[D]'; 185 | } 186 | let modDeleteAll = document.createElement('a'); 187 | modDeleteAll.setAttribute('href', '#'); 188 | modDeleteAll.setAttribute('onclick', 'return smug.modOperation("deleteall", "' + mod + '", "' + postDiv.getAttribute('data-thread') + '", "' + seqno + '", smug.refreshPage)'); 189 | modDeleteAll.innerHTML = '[D+]'; 190 | 191 | let modBan = document.createElement('a'); 192 | modBan.setAttribute('href', '#'); 193 | modBan.setAttribute('onclick', 'return smug.modOperation("ban", "' + mod + '", "' + postDiv.getAttribute('data-thread') + '", "' + seqno + '", smug.refreshPage)'); 194 | modBan.innerHTML = '[B]'; 195 | 196 | let modBanDelete = document.createElement('a'); 197 | modBanDelete.setAttribute('href', '#'); 198 | modBanDelete.setAttribute('onclick', 'return smug.modOperation("bandelete", "' + mod + '", "' + postDiv.getAttribute('data-thread') + '", "' + seqno + '", smug.refreshPage)'); 199 | modBanDelete.innerHTML = '[B&D]'; 200 | 201 | modControls.appendChild(modDelete); 202 | modControls.appendChild(modDeleteAll); 203 | modControls.appendChild(modBan); 204 | modControls.appendChild(modBanDelete); 205 | } 206 | 207 | let postBody = document.createElement('div'); 208 | if (post.body != null){ 209 | postBody.setAttribute('class', 'body'); 210 | let postLine = document.createElement('p'); 211 | postLine.setAttribute('class', 'body-line ltr'); 212 | postLine.innerHTML = decoratePostBody(post.body, indexSeqno, addressObj); 213 | postBody.appendChild(postLine); 214 | } 215 | 216 | postDiv.appendChild(postFullLink); 217 | postDiv.appendChild(postIntro); 218 | postDiv.appendChild(postFiles); 219 | postDiv.appendChild(modControls); 220 | postDiv.appendChild(postBody); 221 | } 222 | 223 | function getCatalogEmptyThread(){//TODO replace with line in page_html.js 224 | var outer = document.createElement('div'); 225 | outer.setAttribute('class', 'mix'); 226 | outer.setAttribute('style', 'display: inline-block;'); 227 | 228 | var inner = document.createElement('div'); 229 | inner.setAttribute('class', 'thread grid-li grid-size-small'); 230 | 231 | outer.appendChild(inner); 232 | 233 | return outer; 234 | } 235 | 236 | function loadCatalog(id, wrappers, addressObj, knownMods){ 237 | var threadStub = document.getElementById('Grid'); 238 | 239 | if (!wrappers[id].sLog) { 240 | return; 241 | } 242 | 243 | let promises = []; 244 | for (let item of wrappers[id].sLog){ 245 | if ((!modMode && item.removed) || !(item.data && item.data.payload && item.data.payload.data && item.data.payload.data.thread)){ 246 | continue; 247 | } 248 | let threadDiv = getCatalogEmptyThread(); 249 | threadDiv.setAttribute('id', item.address); 250 | threadStub.appendChild(threadDiv); 251 | 252 | let threadId = item.data.payload.data.thread.address; 253 | promises.push( 254 | wrapperFuncs.pullWrapperStub(threadId, wrappers).then(() => { 255 | if (wrappers[threadId].last.data.info.mode !== 'thread'){ 256 | return Promise.reject('Item ' + threadId + ' is not a valid thread.'); 257 | } else { 258 | return slog.getSLogObj(wrappers[threadId].last.data.info.op); 259 | //let promisesInner = []; 260 | //promisesInner.push(slog.getSLogObj(wrappers[threadId].last.data.info.op)); 261 | //promisesInner.push(slog.updateSLog(wrappers[threadId].sLog, wrappers[threadId].last.data.head, 3)); 262 | //return Promise.all(promisesInner); 263 | } 264 | }).then(value => { 265 | let sLog = [value]; 266 | //if (values[1][0].data.seqno !== 1){ 267 | //sLog.push(value); 268 | //} 269 | //sLog = sLog.concat(values[1]); 270 | wrappers[threadId].sLog = sLog; 271 | 272 | return loadCatalogStub(threadDiv, threadId, wrappers, addressObj, knownMods, modMode, true, item.data.seqno); 273 | }) 274 | ); 275 | } 276 | return Promise.all(promises); 277 | } 278 | 279 | function loadCatalogStub(threadDiv, id, wrappers, addressObj, knownMods, modMode, fromIndex, indexSeqno){ 280 | for (let mod of knownMods){ 281 | slog.subtractSLog(wrappers[id].sLog, wrappers[mod].sLog); 282 | } 283 | 284 | if (wrappers[id].sLog.length < 1 || wrappers[id].sLog[0].data.seqno !== 1){//OP was deleted, nothing to do 285 | return; 286 | } 287 | 288 | let promises = []; 289 | for (let item of wrappers[id].sLog){ 290 | if ((!modMode && item.removed) || !(item.data && item.data.payload && item.data.payload.data && item.data.payload.data.post)){ 291 | continue; 292 | } 293 | 294 | if (document.getElementById(item.address)){ 295 | //post already exists 296 | continue; 297 | } 298 | 299 | //TODO if (document.getElementById(item.address) && item.removed) - post deleted by a mod, grey it out 300 | 301 | let postDiv = document.createElement('div'); 302 | postDiv.setAttribute('id', item.address); 303 | threadDiv.appendChild(postDiv); 304 | 305 | promises.push( 306 | slog.getSLogObj(item.data.payload.data.post).then(post => { 307 | fillPost(postDiv, post.data, item.data.seqno, item.data.timestamp, addressObj, fromIndex, indexSeqno, modMode, item.removed); 308 | threadDiv.insertBefore(document.createElement('br'), postDiv.nextSibling); 309 | return; 310 | }) 311 | ); 312 | } 313 | 314 | if (fromIndex) { 315 | threadDiv.appendChild(document.createElement('hr')); 316 | } 317 | 318 | return Promise.all(promises); 319 | } 320 | 321 | function loadBoard(id, wrappers, addressObj, knownMods, modMode){ 322 | let threadStub = document.getElementById('thread'); 323 | 324 | if (!wrappers[id].sLog) { 325 | return; 326 | } 327 | 328 | let promises = []; 329 | for (let item of wrappers[id].sLog){ 330 | if ((!modMode && item.removed) || !(item.data && item.data.payload && item.data.payload.data && item.data.payload.data.thread)){ 331 | continue; 332 | } 333 | 334 | let threadDiv = document.createElement('div'); 335 | threadDiv.setAttribute('id', item.address); 336 | threadStub.appendChild(threadDiv); 337 | 338 | let threadId = item.data.payload.data.thread.address; 339 | promises.push( 340 | wrapperFuncs.pullWrapperStub(threadId, wrappers).then(() => { 341 | if (wrappers[threadId].last.data.info.mode !== 'thread'){ 342 | return Promise.reject('Item ' + threadId + ' is not a valid thread.'); 343 | } else { 344 | let promisesInner = []; 345 | promisesInner.push(slog.getSLogObj(wrappers[threadId].last.data.info.op)); 346 | promisesInner.push(slog.updateSLog(wrappers[threadId].sLog, wrappers[threadId].last.data.head, 3)); 347 | return Promise.all(promisesInner); 348 | } 349 | }).then(values => { 350 | let sLog = []; 351 | if (values[1][0].data.seqno !== 1){ 352 | sLog.push(values[0]); 353 | } 354 | sLog = sLog.concat(values[1]); 355 | wrappers[threadId].sLog = sLog; 356 | 357 | return loadThread(threadDiv, threadId, wrappers, addressObj, knownMods, modMode, true, item.data.seqno); 358 | }) 359 | ); 360 | } 361 | return Promise.all(promises); 362 | } 363 | 364 | function loadThread(threadDiv, id, wrappers, addressObj, knownMods, modMode, fromIndex, indexSeqno){ 365 | for (let mod of knownMods){ 366 | slog.subtractSLog(wrappers[id].sLog, wrappers[mod].sLog); 367 | } 368 | 369 | if (wrappers[id].sLog.length < 1 || wrappers[id].sLog[0].data.seqno !== 1){//OP was deleted, nothing to do 370 | return; 371 | } 372 | 373 | if (threadDiv == null) { 374 | threadDiv = document.getElementById('thread'); 375 | } 376 | 377 | let promises = []; 378 | for (let item of wrappers[id].sLog){ 379 | if ((!modMode && item.removed) || !(item.data && item.data.payload && item.data.payload.data && item.data.payload.data.post)){ 380 | continue; 381 | } 382 | 383 | if (document.getElementById(item.address)){ 384 | //post already exists 385 | continue; 386 | } 387 | 388 | //TODO if (document.getElementById(item.address) && item.removed) - post deleted by a mod, grey it out 389 | 390 | let postDiv = document.createElement('div'); 391 | postDiv.setAttribute('id', item.address); 392 | threadDiv.appendChild(postDiv); 393 | 394 | promises.push( 395 | slog.getSLogObj(item.data.payload.data.post).then(post => { 396 | fillPost(postDiv, post.data, item.data.seqno, item.data.timestamp, addressObj, fromIndex, indexSeqno, modMode, item.removed); 397 | threadDiv.insertBefore(document.createElement('br'), postDiv.nextSibling); 398 | return; 399 | }) 400 | ); 401 | } 402 | 403 | if (fromIndex) { 404 | threadDiv.appendChild(document.createElement('hr')); 405 | } 406 | 407 | return Promise.all(promises); 408 | } 409 | 410 | function setTemplateHTML(){ 411 | let htmlHeadContent = pageHtml.css; 412 | htmlHeadContent += pageHtml.favicon; 413 | let htmlBodyContent = pageHtml.boardlist; 414 | //htmlBodyContent += pageHtml.loadIndicator; 415 | //htmlBodyContent += pageHtml.header 416 | 417 | document.head.innerHTML = htmlHeadContent; 418 | document.body.innerHTML = htmlBodyContent; 419 | } 420 | 421 | function setHeader(wrappers, addressObj){ 422 | let title = null; 423 | let uri = null; 424 | let fromSite = false; 425 | for (let item of addressObj){ 426 | if (item.mode === 'site'){ 427 | title = wrappers[item.actualAddress].last.data.info.title; 428 | fromSite = true; 429 | } 430 | } 431 | for (let item of addressObj){ 432 | if (item.mode === 'board'){ 433 | title = wrappers[item.actualAddress].last.data.info.title; 434 | uri = item.publicAddress; 435 | fromSite = false; 436 | } 437 | } 438 | setTitle(title, uri, fromSite); 439 | } 440 | 441 | function setNavLinks(inThread, addressObj){ 442 | if (inThread){ 443 | document.getElementById('thread-return').setAttribute('href', functions.getNewHash(addressObj, ['thread', 'post'])); 444 | } 445 | document.getElementById('catalog-link').setAttribute('href', functions.getNewHash(addressObj, ['post', 'thread', 'catalog'], ['catalog'])); 446 | document.getElementById('index-link').setAttribute('href', functions.getNewHash(addressObj, ['post', 'thread', 'catalog'])); 447 | } 448 | 449 | function setPostFormData(id){ 450 | //if (item.mode === 'board' || item.mode === 'thread') { 451 | document.getElementById('submit').setAttribute('onclick', 'smug.submitPost("' + id + '", smug.wrappers, "postform")'); 452 | } 453 | 454 | function setTitle(title, uri, fromSite){ 455 | document.getElementById('heading').innerHTML = ''; 456 | if (uri != null && title != null) { 457 | document.getElementById('heading').appendChild(document.createTextNode('/' + uri + '/ - ' + title)); 458 | } else if (title != null) { 459 | if (fromSite) { 460 | document.getElementById('heading').appendChild(document.createTextNode(title)); 461 | } else { 462 | document.getElementById('heading').appendChild(document.createTextNode('[Detached Board] - ' + title)); 463 | } 464 | } else { 465 | document.getElementById('heading').appendChild(document.createTextNode('[Detached Thread]')); 466 | } 467 | } 468 | 469 | function setSiteHTML(){ 470 | document.body.innerHTML += pageHtml.siteBoardlistHeader; 471 | document.body.innerHTML += pageHtml.siteBoardlist; 472 | 473 | //TODO hide load indicator 474 | 475 | } 476 | 477 | function setBoardHTML(){ 478 | document.body.innerHTML += pageHtml.header; 479 | document.body.innerHTML += pageHtml.banner; 480 | document.body.innerHTML += pageHtml.uploadForm; 481 | document.body.innerHTML += pageHtml.threadStub; 482 | document.getElementById('submit').innerHTML = 'New Thread'; 483 | 484 | } 485 | 486 | function setCatalogHTML(){ 487 | document.body.innerHTML += pageHtml.header; 488 | document.body.innerHTML += pageHtml.banner; 489 | document.body.innerHTML += pageHtml.uploadForm; 490 | document.body.innerHTML += pageHtml.catalogStub; 491 | document.body.classList.value = 'theme-catalog active-catalog'; 492 | document.getElementById('submit').innerHTML = 'New Thread'; 493 | } 494 | 495 | function setThreadHTML(){ 496 | document.body.innerHTML += pageHtml.header; 497 | document.body.innerHTML += pageHtml.banner; 498 | document.body.innerHTML += pageHtml.uploadForm; 499 | document.body.innerHTML += pageHtml.threadStub; 500 | document.body.innerHTML += pageHtml.threadControls; 501 | 502 | } 503 | 504 | function setEditorHTML(){ 505 | document.body.innerHTML += pageHtml.objectEditorHeader; 506 | } 507 | 508 | function fillBoardlist(sitePublicAddress, siteSLog){ 509 | document.getElementById('boardlist-home').setAttribute('href', '#' + sitePublicAddress); 510 | document.getElementById('boardlist-create').setAttribute('href', '#' + sitePublicAddress + '/create'); 511 | document.getElementById('boardlist-options').setAttribute('href', '#' + sitePublicAddress + '/options'); 512 | document.getElementById('boardlist-manage').setAttribute('href', '#' + sitePublicAddress + '/manage'); 513 | 514 | let items = []; 515 | for (let entry of siteSLog){ 516 | if (!entry.removed && entry.data && entry.data.payload && entry.data.payload.data && entry.data.payload.data.uri){ 517 | items.push(entry.data.payload.data.uri); 518 | } 519 | } 520 | 521 | let firstBoard = true; 522 | let boards = document.getElementById('boardlist-boards'); 523 | boards.innerHTML = ' ['; 524 | for (let item of items){ 525 | boards.innerHTML += firstBoard ? ' ' : ' / '; 526 | firstBoard = false; 527 | 528 | let link = document.createElement('a'); 529 | link.setAttribute('href', '#' + sitePublicAddress + '/' + item); 530 | link.appendChild(document.createTextNode(item)); 531 | boards.appendChild(link); 532 | } 533 | boards.innerHTML += ' ]'; 534 | } 535 | 536 | function fillBoardlistTable(sitePublicAddress, id, wrappers){ 537 | let boardTable = document.getElementById('boardlist-table'); 538 | 539 | let headBoard = document.createElement('th'); 540 | headBoard.appendChild(document.createTextNode('Board')); 541 | let headTitle = document.createElement('th'); 542 | headTitle.appendChild(document.createTextNode('Title')); 543 | let headAddress = document.createElement('th'); 544 | headAddress.appendChild(document.createTextNode('Address')); 545 | 546 | let headRow = document.createElement('tr'); 547 | headRow.appendChild(headBoard); 548 | headRow.appendChild(headTitle); 549 | headRow.appendChild(headAddress); 550 | boardTable.appendChild(headRow); 551 | 552 | let payloads = []; 553 | for (let entry of wrappers[id].sLog){ 554 | if (!entry.removed && entry.data && entry.data.payload && entry.data.payload.data && entry.data.payload.data.address){ 555 | payloads.push(entry.data.payload); 556 | } 557 | } 558 | 559 | let promises = []; 560 | for (let payload of payloads){ 561 | let uri = document.createElement('td'); 562 | let title = document.createElement('td'); 563 | let address = document.createElement('td'); 564 | 565 | let link = document.createElement('a'); 566 | link.setAttribute('href', '#' + sitePublicAddress + '/' + payload.data.uri); 567 | link.appendChild(document.createTextNode('/' + payload.data.uri + '/')); 568 | uri.appendChild(link); 569 | 570 | let rawLink = document.createElement('a'); 571 | rawLink.setAttribute('href', '#' + payload.data.address); 572 | rawLink.appendChild(document.createTextNode(payload.data.address)); 573 | address.appendChild(rawLink); 574 | 575 | let row = document.createElement('tr'); 576 | row.appendChild(uri); 577 | row.appendChild(title); 578 | row.appendChild(address); 579 | boardTable.appendChild(row); 580 | 581 | promises.push( 582 | wrapperFuncs.pullWrapperStub(payload.data.address, wrappers).then(() => { 583 | title.appendChild(document.createTextNode(wrappers[payload.data.address].last.data.info.title)); 584 | return; 585 | }) 586 | ); 587 | } 588 | return Promise.all(promises); 589 | } 590 | module.exports.setSiteHTML = setSiteHTML; 591 | module.exports.setBoardHTML = setBoardHTML; 592 | module.exports.setCatalogHTML = setCatalogHTML; 593 | module.exports.setThreadHTML = setThreadHTML; 594 | module.exports.setEditorHTML = setEditorHTML; 595 | module.exports.loadBoard = loadBoard; 596 | module.exports.loadCatalog = loadCatalog; 597 | module.exports.loadThread = loadThread; 598 | module.exports.setTemplateHTML = setTemplateHTML; 599 | module.exports.setHeader = setHeader; 600 | module.exports.setNavLinks = setNavLinks; 601 | module.exports.setPostFormData = setPostFormData; 602 | module.exports.setTitle = setTitle; 603 | module.exports.fillBoardlist = fillBoardlist; 604 | module.exports.fillBoardlistTable = fillBoardlistTable; 605 | -------------------------------------------------------------------------------- /client/inc/pagehtml.js: -------------------------------------------------------------------------------- 1 | /* General HTML */ 2 | 3 | var favicon = ''; 4 | 5 | var css = ''; 6 | 7 | var boardlist = '
[ Home / Create / Manage ] [][Options]
'; 8 | 9 | var loadIndicator = '

Loading...
'; 10 | 11 | /* Thread HTML */ 12 | 13 | var header = '

Index  Catalog

'; 14 | 15 | var banner = '
Posting mode: Reply
'; 16 | 17 | var uploadForm = '
Name
Email
Subject
Comment *
File
'; 18 | 19 | var threadStub = '
'; 20 | 21 | var threadControls = '

[Return][Go to top][Update] ( Auto)



'; 22 | 23 | /* Catalog HTML */ 24 | 25 | var catalogStub = '
'; 26 | 27 | /* Site HTML (create page) */ 28 | 29 | var siteModCreateHeader = '

Moderator Creation

'; 30 | 31 | var siteModCreateForm = '
Name
Password
'; 32 | 33 | var siteBoardCreateHeader = '

Board Creation

'; 34 | 35 | var siteBoardCreateForm = '
Title
Thread server(s)/ipns// (must be 46 chars)
Moderator(s)/ipns// (must be 46 chars)
Banner(s)/ipns// (must be 46 chars)
';//TODO need to have way to put multiple things in each field 36 | 37 | var siteAddHeader = '

Board Registration

'; 38 | 39 | var siteAddForm = '
URI// (must be all lowercase or numbers and < 30 chars)
Address/ipns// (must be 46 chars)
'; 40 | 41 | var settingsPage = '

Local Settings

'; 42 | 43 | /* Site HTML (Board list) */ 44 | 45 | var siteBoardlistHeader = '

Smugchan

'; 46 | 47 | var siteBoardlist = '
'; 48 | 49 | /* Object editor */ 50 | 51 | var objectEditorHeader = '

Object Editor

'; 52 | 53 | /* Mod stuff */ 54 | 55 | var optionsHeader = '

Mod Management

'; 56 | 57 | var optionsForm = '
Address/ipns// (must be 46 chars)
Password
'; 58 | 59 | module.exports.favicon = favicon; 60 | module.exports.css = css; 61 | module.exports.boardlist = boardlist; 62 | module.exports.loadIndicator = loadIndicator; 63 | module.exports.header = header; 64 | module.exports.banner = banner; 65 | module.exports.uploadForm = uploadForm; 66 | module.exports.threadStub = threadStub; 67 | module.exports.catalogStub = catalogStub; 68 | module.exports.threadControls = threadControls; 69 | module.exports.siteModCreateHeader = siteModCreateHeader; 70 | module.exports.siteModCreateForm = siteModCreateForm; 71 | module.exports.siteBoardCreateHeader = siteBoardCreateHeader; 72 | module.exports.siteBoardCreateForm = siteBoardCreateForm; 73 | module.exports.siteAddHeader = siteAddHeader; 74 | module.exports.siteAddForm = siteAddForm; 75 | module.exports.settingsPage = settingsPage; 76 | module.exports.siteBoardlistHeader = siteBoardlistHeader; 77 | module.exports.siteBoardlist = siteBoardlist; 78 | module.exports.objectEditorHeader = objectEditorHeader; 79 | module.exports.optionsHeader = optionsHeader; 80 | module.exports.optionsForm = optionsForm; 81 | 82 | 83 | -------------------------------------------------------------------------------- /client/inc/post.js: -------------------------------------------------------------------------------- 1 | var wrapperFuncs = require('../../common/wrapperfuncs.js'); 2 | var util = require('../../common/util.js'); 3 | //var sio = require('../../common/sio.js'); 4 | var controller = require('./controller.js'); 5 | var bridge = require('./serverbridge.js'); 6 | 7 | function submitPost(wrapperId, wrappers, formName){ 8 | 9 | //TODO grey out the button 10 | let formData = new FormData(document.getElementById(formName)); 11 | let formKeys = formData.keys(); 12 | let postData = {}; 13 | 14 | for (let key of formKeys){ 15 | postData[key] = formData.get(key); 16 | } 17 | 18 | 19 | if (postData.author === '') { 20 | postData.author = 'Anonymous'; 21 | } 22 | postData.type = 'add'; 23 | 24 | Promise.resolve().then(() => { 25 | if (!wrappers[wrapperId]){ 26 | return wrapperFuncs.pullWrapperStub(wrapperId, wrappers); 27 | } else { 28 | return; 29 | } 30 | }).then(() => { 31 | //work out where we are 32 | if (!util.propertyExists(wrappers[wrapperId], 'last.data.info.mode')){ 33 | Promise.reject('Not a valid object.'); 34 | } else if (wrappers[wrapperId].last.data.info.mode === 'board'){ 35 | //if we're at a board, we're creating a thread, posting to that thread, then posting that thread to a board server 36 | if (wrappers[wrapperId].last.data.info.threadservers && wrappers[wrapperId].last.data.info.threadservers.length > 0){ 37 | //create a new thread 38 | let chosenServer = wrappers[wrapperId].last.data.info.threadservers[Math.floor(Math.random() * wrappers[wrapperId].last.data.info.threadservers.length)]; 39 | let threadData = { 40 | mode: 'thread', 41 | type: 'create', 42 | id: chosenServer, 43 | server: chosenServer 44 | }; 45 | 46 | return bridge.sendToServer(chosenServer, wrappers, threadData).then(threadAddress => { 47 | let promises = []; 48 | //post to that thread 49 | postData.mode = 'thread'; 50 | postData.id = threadAddress; 51 | promises.push(bridge.sendToServer(threadAddress, wrappers, postData)); 52 | 53 | //simultaneously, add that thread to the board in question 54 | let boardData = { 55 | mode: 'board', 56 | type: 'add', 57 | id: wrapperId, 58 | newaddress: threadAddress 59 | }; 60 | promises.push(bridge.sendToServer(wrapperId, wrappers, boardData)); 61 | 62 | Promise.all(promises).then(values => { 63 | return values; 64 | }); 65 | }); 66 | } else { 67 | Promise.reject('No server found.'); 68 | } 69 | } else if (wrappers[wrapperId].last.data.info.mode === 'thread'){ 70 | //if we're at a thread, we're posting to that thread's server. 71 | if (!wrappers[wrapperId].last.data.info.server){ 72 | Promise.reject('No server found.'); 73 | } else { 74 | postData.mode = wrappers[wrapperId].last.data.info.mode; 75 | postData.id = wrapperId; 76 | return bridge.sendToServer(wrappers[wrapperId].last.data.info.server, wrappers, postData).then(result => { 77 | controller.queueRefresh(); 78 | return result; 79 | }); 80 | } 81 | } else { 82 | Promise.reject('Can\'t post to that object'); 83 | } 84 | 85 | //need to return the serverId we're using 86 | }).then(console.log).catch(console.error); 87 | 88 | //TODO ungrey the button 89 | 90 | //if we're at a board, we're posting to a thread server, then posting to a board server 91 | //send the post to the server at [actualAddress].last.data.info.threadservers, perhaps randomly selecting which 92 | //-> need to get the actual address of the thread server. it'd be nice if we already had it loaded 93 | //-> if a thread server fails, it might be good to go on to the next thread server? //yes, it'd be very good, almost a requirement in fact //TODO 94 | //create a new thread at that thread server 95 | //get back the address of the new thread then make the actual post to that thread 96 | //simultaneously, add the thread to the board server 97 | //once both are done, get back the assigned seqno, then add it to the parsed address stream, then refresh the page to there 98 | 99 | //if we're at a thread, we're posting to that thread server. 100 | //send the post to the server at [actualAddress].last.data.info.server 101 | //TODO get back the assigned seqno. if we have a 'post' in addressParsed, replace it. if we don't, add it to the address 102 | //TODO trigger page refresh, perhaps in ~3 seconds 103 | //TODO scroll to the post when the refresh is complete - which we should be tracking 104 | } 105 | 106 | module.exports.submitPost = submitPost; 107 | -------------------------------------------------------------------------------- /client/inc/serverbridge.js: -------------------------------------------------------------------------------- 1 | var wrapperFuncs = require('../../common/wrapperfuncs.js'); 2 | var inputHandler = require('../../common/inputhandler.js'); 3 | 4 | //TODO track local servers 5 | var localWrappers = {};//should this perhaps just be integrated into the standard wrapper construct? perhaps with wrappers[id].local == true ? 6 | 7 | function isLocalServer(serverId){ 8 | if (localWrappers[serverId] != null){ 9 | return true; 10 | } else { 11 | return false; 12 | } 13 | } 14 | 15 | function sendToRemoteServer(serverId, wrappers, formData){ 16 | return new Promise((resolve, reject) => { 17 | wrapperFuncs.getServerConn(serverId, wrappers).then(conn => {//TODO just return this? 18 | let req = new XMLHttpRequest();//TODO try transitioning this to the xhr library, don't know if doable with files 19 | req.onreadystatechange = () => { 20 | if (req.readyState === XMLHttpRequest.DONE) { 21 | resolve(req.responseText);//TODO should resolve an obj with the same format as from inputHandler 22 | } 23 | }; 24 | req.addEventListener('error', err => { 25 | reject(err); 26 | }); 27 | req.open('POST', conn.ip); 28 | req.send(formData); 29 | }).catch(reject); 30 | }); 31 | } 32 | 33 | function sendToLocalServer(){ 34 | //TODO 35 | let reader = new FileReader(); 36 | return inputHandler.handleInput(localWrappers, msg, file, remoteAddress, serverAddress); 37 | } 38 | 39 | function objToFormData(obj){//TODO move to functions 40 | let formData = new FormData(); 41 | for (let item in obj){ 42 | formData.set(item, obj[item]); 43 | } 44 | return formData; 45 | } 46 | 47 | function sendToServer(serverId, wrappers, formObj){ 48 | //determine if the server is local to the current browser 49 | if (isLocalServer(serverId)){ 50 | //if so, send to the local server 51 | return sendToLocalServer(); 52 | } else { 53 | //if not, send to the remote server 54 | let formData = objToFormData(formObj); 55 | return sendToRemoteServer(serverId, wrappers, formData); 56 | } 57 | } 58 | 59 | module.exports.sendToServer = sendToServer; 60 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | If the page gets stuck here, you probably haven't configured IPFS correctly. Go have a look at the instructions and try again. 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/smugchan.js: -------------------------------------------------------------------------------- 1 | var functions = require('./inc/functions.js'); 2 | //var pageHtml = require('./inc/pagehtml.js'); 3 | var page = require('./inc/page.js'); 4 | var post = require('./inc/post.js'); 5 | var controller = require('./inc/controller.js'); 6 | //var sio = require('../common/sio.js'); 7 | 8 | var globalWrappers = {}; 9 | var addressParsed = []; 10 | 11 | function loadPage(){ 12 | controller.haltRefresh(); 13 | 14 | var newAddressParsed = functions.parseLocationAddress(location.hash); 15 | 16 | var isNewPage = false; 17 | if (addressParsed === []) { 18 | isNewPage = true; 19 | //TODO work out if we're on the same page 20 | //if the last item was a thread, and we've just added an extra item, then it's the same page 21 | //if the last item was a post, and we've just changed the item at that position, then it's the same page 22 | //otherwise it's a different page 23 | } 24 | isNewPage = true; 25 | 26 | if (isNewPage){ 27 | //globalWrappers = {};//TODO get rid of this? 28 | 29 | page.setTemplateHTML(); 30 | 31 | window.smug.addressObj = addressParsed = newAddressParsed; 32 | window.smug.knownMods = []; 33 | 34 | 35 | controller.handleWrapperRecursive(addressParsed, globalWrappers, 0, false, window.smug.knownMods); 36 | } else { 37 | //TODO scroll to element 38 | } 39 | } 40 | 41 | document.addEventListener('DOMContentLoaded', () => { 42 | var smug = window.smug || {}; 43 | smug.submitPost = post.submitPost; 44 | smug.wrappers = globalWrappers; 45 | smug.addressObj = addressParsed; 46 | smug.forceRefresh = controller.forceRefresh; 47 | window.smug = smug; 48 | loadPage(); 49 | }, false); 50 | 51 | window.addEventListener('hashchange', loadPage, false); 52 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #eef2ff url("img/fade-blue.png") repeat-x scroll 50% 0; 3 | color: black; 4 | font-family: arial,helvetica,sans-serif; 5 | font-size: 10pt; 6 | margin: 0 4px; 7 | padding-left: 4px; 8 | padding-right: 4px; 9 | padding-top: 20px; 10 | } 11 | 12 | img.banner, img.board_image { 13 | border: 1px solid #a9a9a9; 14 | display: block; 15 | margin: 12px auto 0; 16 | } 17 | 18 | header { 19 | margin: 1em 0; 20 | } 21 | 22 | header div.subtitle, h1 { 23 | color: #af0a0f; 24 | text-align: center; 25 | } 26 | header div.subtitle { 27 | font-size: 8pt; 28 | } 29 | 30 | h1 { 31 | font-family: tahoma; 32 | font-size: 20pt; 33 | letter-spacing: -2px; 34 | margin: 0; 35 | } 36 | 37 | div.banner, div.banner a { 38 | color: white; 39 | } 40 | 41 | div.banner { 42 | background-color: #e04000; 43 | font-size: 12pt; 44 | font-weight: bold; 45 | margin: 1em 0; 46 | text-align: center; 47 | } 48 | 49 | 50 | .boardlist-table { 51 | display: table; 52 | margin: -2px; 53 | margin-bottom: 10px; 54 | overflow: hidden; 55 | table-layout: fixed; 56 | } 57 | 58 | #boardlist-table { 59 | /*width: 100%;*/ 60 | margin-left:auto; 61 | margin-right:auto; 62 | } 63 | 64 | #boardlist-table td { 65 | margin: 0; 66 | padding: 4px 15px 4px 4px; 67 | 68 | text-align: left; 69 | } 70 | #boardlist-table th { 71 | border: 1px solid #000333; 72 | padding: 4px 15px 5px 5px; 73 | 74 | background: #98E; 75 | color: #000333; 76 | text-align: left; 77 | white-space: nowrap; 78 | } 79 | 80 | 81 | #post-form-outer { 82 | text-align: center; 83 | } 84 | 85 | #post-form-inner { 86 | display: inline-block; 87 | } 88 | 89 | .post-table, .post-table-options, textarea { 90 | width: 100%; 91 | } 92 | 93 | form table { 94 | margin: auto; 95 | font-size: 10pt; 96 | } 97 | 98 | #post-form-inner .post-table tr { 99 | background-color: transparent; 100 | } 101 | 102 | .post-table th, .post-table-options th { 103 | width: 85px; 104 | } 105 | 106 | form table tr th { 107 | background: #98e none repeat scroll 0 0; 108 | } 109 | 110 | form table tr th { 111 | padding: 4px; 112 | text-align: left; 113 | } 114 | 115 | form table tr td { 116 | margin: 0; 117 | padding: 0; 118 | text-align: left; 119 | } 120 | 121 | input[type="text"], input[type="password"], textarea { 122 | border: 1px solid #a9a9a9; 123 | font-family: sans-serif; 124 | font-size: inherit; 125 | text-indent: 0; 126 | text-shadow: none; 127 | text-transform: none; 128 | word-spacing: normal; 129 | } 130 | 131 | form table input { 132 | height: auto; 133 | } 134 | 135 | .required-star { 136 | color: maroon; 137 | } 138 | 139 | hr { 140 | -moz-border-bottom-colors: none; 141 | -moz-border-left-colors: none; 142 | -moz-border-right-colors: none; 143 | -moz-border-top-colors: none; 144 | border-color: #b7c5d9; 145 | border-image: none; 146 | border-style: solid none none; 147 | border-width: 1px medium medium; 148 | clear: left; 149 | height: 0; 150 | } 151 | 152 | div.post.reply div.body a { 153 | color: #D00; 154 | } 155 | 156 | a, a:visited { 157 | color: #34345c; 158 | text-decoration: underline; 159 | } 160 | 161 | a.post_no { 162 | margin: 0; 163 | padding: 0; 164 | text-decoration: none; 165 | } 166 | 167 | /* Board List */ 168 | div.boardlist { 169 | margin-top: 3px; 170 | 171 | color: #89A; 172 | font-size: 9pt; 173 | } 174 | div.boardlist.bottom { 175 | margin-top: 12px; 176 | clear: both; 177 | } 178 | div.boardlist a { 179 | text-decoration: none; 180 | } 181 | 182 | div.boardlist:not(.bottom) { 183 | position: fixed; 184 | top: 0; 185 | left: 0; 186 | right: 0; 187 | margin-top: 0; 188 | z-index: 30; 189 | box-shadow: 0 1px 2px rgba(0, 0, 0, .15); 190 | border-bottom: 1px solid; 191 | background-color: #D6DAF0; 192 | } 193 | 194 | 195 | div.post.op { 196 | margin-bottom: 5px; 197 | margin-right: 20px; 198 | } 199 | 200 | div.post.reply.flagged { 201 | background: #a1a0ab; 202 | } 203 | 204 | div.post.reply { 205 | background: #d6daf0 none repeat scroll 0 0; 206 | border-color: #b7c5d9; 207 | border-style: none solid solid none; 208 | border-width: 1px; 209 | display: inline-block; 210 | margin: 0.2em 4px; 211 | max-width: 94% !important; 212 | padding: 0.5em 0.3em 0.5em 0.6em; 213 | } 214 | 215 | div.post .post-image { 216 | padding: 5px; 217 | margin: 0 20px 0 0; 218 | } 219 | 220 | .post-image { 221 | display: block; 222 | float: left; 223 | border: none; 224 | } 225 | 226 | div.post p.fileinfo { 227 | padding-left: 5px; 228 | } 229 | 230 | div.post.reply div.body { 231 | margin-left: 1.8em; 232 | } 233 | 234 | div.post.reply { 235 | min-width: 33%; 236 | } 237 | 238 | div.post div.body { 239 | white-space: pre-wrap; 240 | word-wrap: break-word; 241 | } 242 | 243 | div.post div.body { 244 | margin-top: 0.8em; 245 | padding-bottom: 0.3em; 246 | padding-right: 3em; 247 | } 248 | 249 | div.post p { 250 | display: block; 251 | margin: 0; 252 | line-height: 1.16em; 253 | font-size: 13px; 254 | min-height: 1.16em; 255 | } 256 | 257 | .unimportant, .unimportant * { 258 | font-size: 10px; 259 | } 260 | 261 | .intro { 262 | margin-left: 10px; 263 | padding: 0 0 0.2em; 264 | } 265 | 266 | .intro label { 267 | display: inline; 268 | } 269 | 270 | .intro a.post_no { 271 | color: inherit; 272 | } 273 | 274 | .intro span, .intro a.email { 275 | margin-right: 5px; 276 | } 277 | 278 | .intro span.subject, .intro .reply_link { 279 | margin-left: 5px; 280 | } 281 | 282 | .intro span.subject, .intro a.email { 283 | color: #0f0c5d; 284 | font-weight: bold; 285 | } 286 | 287 | .intro span.name { 288 | color: #117743; 289 | font-weight: bold; 290 | } 291 | 292 | span.heading { 293 | color: #AF0A0F; 294 | font-size: 11pt; 295 | font-weight: bold; 296 | } 297 | 298 | span.spoiler:hover,div.post.reply div.body span.spoiler:hover a { 299 | color: white; 300 | } 301 | 302 | span.spoiler { 303 | background: black; 304 | color: black; 305 | padding: 0 1px; 306 | } 307 | 308 | span.controls { 309 | float: right; 310 | margin: 0; 311 | padding: 0; 312 | font-size: 80%; 313 | } 314 | 315 | span.controls.op { 316 | float: none; 317 | margin-left: 10px; 318 | } 319 | 320 | span.controls a { 321 | margin: 0; 322 | } 323 | 324 | #thread-interactions { 325 | clear: both; 326 | margin: 8px 0; 327 | } 328 | 329 | #thread-links { 330 | float: left; 331 | } 332 | 333 | #thread-links > a { 334 | padding-right: 10px; 335 | } 336 | 337 | 338 | .theme-catalog div.thread img { 339 | float: none!important; 340 | margin: auto; 341 | max-height: 150px; 342 | max-width: 200px; 343 | box-shadow: 0 0 4px rgba(0,0,0,0.55); 344 | border: 2px solid rgba(153,153,153,0); 345 | } 346 | 347 | .theme-catalog div.thread { 348 | display: inline-block; 349 | vertical-align: top; 350 | text-align: center; 351 | font-weight: normal; 352 | margin-top: 2px; 353 | margin-bottom: 2px; 354 | padding: 2px; 355 | height: 300px; 356 | width: 205px; 357 | overflow: hidden; 358 | position: relative; 359 | font-size: 11px; 360 | max-height: 300px; 361 | background: rgba(182, 182, 182, 0.12); 362 | border: 2px solid rgba(111, 111, 111, 0.34); 363 | max-height:300px; 364 | } 365 | 366 | .theme-catalog div.thread strong { 367 | display: block; 368 | } 369 | 370 | .theme-catalog div.threads { 371 | text-align: center; 372 | margin-left: -20px; 373 | } 374 | 375 | .theme-catalog div.thread:hover { 376 | background: #D6DAF0; 377 | border-color: #B7C5D9; 378 | } 379 | 380 | .theme-catalog div.grid-size-vsmall img { 381 | max-height: 33%; 382 | max-width: 95% 383 | } 384 | 385 | .theme-catalog div.grid-size-vsmall { 386 | min-width:90px; max-width: 90px; 387 | max-height: 148px; 388 | } 389 | 390 | .theme-catalog div.grid-size-small img { 391 | max-height: 33%; 392 | max-width: 95% 393 | } 394 | 395 | .theme-catalog div.grid-size-small { 396 | min-width:140px; max-width: 140px; 397 | max-height: 192px; 398 | } 399 | 400 | .theme-catalog div.grid-size-medium img { 401 | max-height: 33%; 402 | max-width: 95% 403 | } 404 | 405 | .theme-catalog div.grid-size-medium { 406 | min-width:200px; max-width: 200px; 407 | max-height: 274px; 408 | } 409 | 410 | .theme-catalog div.grid-size-large img { 411 | max-height: 40%; 412 | max-width: 95% 413 | } 414 | 415 | .theme-catalog div.grid-size-large { 416 | min-width: 256px; max-width: 256px; 417 | max-height: 384px; 418 | } 419 | 420 | .theme-catalog img.thread-image { 421 | height: auto; 422 | max-width: 100%; 423 | } 424 | 425 | @media (max-width: 420px) { 426 | .theme-catalog ul#Grid { 427 | padding-left: 18px; 428 | } 429 | 430 | .theme-catalog div.thread { 431 | width: auto; 432 | margin-left: 0; 433 | margin-right: 0; 434 | } 435 | 436 | .theme-catalog div.threads { 437 | overflow: hidden; 438 | } 439 | div.post .body { 440 | clear: both; 441 | } 442 | } 443 | 444 | -------------------------------------------------------------------------------- /common/inputhandler.js: -------------------------------------------------------------------------------- 1 | var sio = require('./sio.js'); 2 | var storage = require('./storage.js'); 3 | var util = require('./util.js'); 4 | var wrapperFuncs = require('./wrapperfuncs.js'); 5 | 6 | var serverModes = ['site', 'board', 'thread', 'mod', 'server']; 7 | 8 | var formats = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'webm', 'mp4', 'mp3', 'ogg', 'opus', 'flac', 'apng', 'pdf', 'iso', 'zip', 'tar', 'gz', 'rar', '7z', 'torrent']; 9 | var previewable = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico'];//things image-size can deal with 10 | 11 | function isValidRequest(mode, type, password, key, wrappers){ 12 | let requestValid = false; 13 | let authorized = false; 14 | if (key != null && wrappers[key] != null && wrappers[key].password === password) { 15 | authorized = true; 16 | } 17 | 18 | //authorized = true; //TODO remove this, for test purposes only 19 | 20 | if (mode == null || type == null || serverModes.indexOf(mode) === -1){ 21 | return false; 22 | } else if (type === 'create') { 23 | requestValid = true; 24 | } else if (type === 'add' && (mode !== 'mod' || authorized)) { 25 | requestValid = true; 26 | } else if (type === 'remove' && authorized) { 27 | requestValid = true; 28 | } else if (type === 'set' && authorized) { 29 | requestValid = true; 30 | } else if (type === 'delete' && authorized) { 31 | requestValid = true; 32 | } 33 | 34 | return requestValid; 35 | } 36 | 37 | //handler (generic) 38 | function handleInput(wrappers, msg, files, sender, serverAddress){ 39 | return new Promise((resolve, reject) => { 40 | if (isValidRequest(msg.mode, msg.type, msg.password, msg.id, wrappers)){ 41 | let wrapperOperation; 42 | switch(msg.type){ 43 | case 'create': 44 | wrapperOperation = wrapperFuncs.createObj(msg, wrappers, serverAddress); 45 | break; 46 | case 'add': 47 | wrapperOperation = wrapperFuncs.addToObj(msg, wrappers, files, formats, previewable); 48 | break; 49 | case 'remove': 50 | wrapperOperation = wrapperFuncs.removeFromObj(msg, wrappers); 51 | break; 52 | case 'set': 53 | wrapperOperation = wrapperFuncs.setObj(msg, wrappers, serverAddress); 54 | break; 55 | case 'delete': 56 | wrapperOperation = wrapperFuncs.deleteObj(msg, wrappers); 57 | } 58 | 59 | wrapperOperation.then(resp => { 60 | storage.saveWrappers(util.prepareStorageKeys(wrappers)); //TODO put this somewhere else 61 | resolve(resp); 62 | }).catch(reject); 63 | } else { 64 | reject({status: 400, result: 'Invalid request or password.'}); 65 | } 66 | }); 67 | } 68 | 69 | function setupServer(wrappers){ 70 | return Promise.all([storage.getWrappers(), sio.getKeys()]).then((values) => { 71 | wrappers = util.rebuildKeys(values[0], values[1]); 72 | 73 | /*console.log('Test point A'); 74 | console.log(wrappers); 75 | let board; 76 | handleInput({mode: 'board', type: 'create', password: 'fugg', title: 'My Shitpoos', formats: ['png', 'jpg'], mods: []}, [null], 'hurr', 'http://localhost').then(obj => { 77 | console.log('Test point B'); 78 | console.log(obj); 79 | board = obj.result; 80 | console.log(wrappers); 81 | return handleInput({mode: 'thread', type: 'create', password: 'fugg', title: 'My Shitthread'}, [null], 'hurr', 'http://localhost'); 82 | }).then(obj => { 83 | console.log('Test point C'); 84 | console.log(obj); 85 | console.log(wrappers); 86 | return handleInput({mode: 'board', type: 'add', id: board, password: 'fugg', newaddress: obj.result}, [null], 'hurr', 'http://localhost'); 87 | }).then(obj => { 88 | console.log('Test point D'); 89 | console.log(obj); 90 | console.log(wrappers); 91 | return handleInput({mode: 'board', type: 'add', id: board, password: 'fugg', newaddress: obj.result}, [null], 'hurr', 'http://localhost'); 92 | }).then(obj => { 93 | console.log('Test point E'); 94 | console.log(obj); 95 | console.log(wrappers); 96 | return handleInput({mode: 'board', type: 'add', id: board, password: 'fugg', newaddress: obj.result}, [null], 'hurr', 'http://localhost'); 97 | }).then(obj => { 98 | console.log('Test point F'); 99 | console.log(obj); 100 | console.log(wrappers); 101 | }).catch(console.error);*/ 102 | 103 | /*var server; 104 | var mod; 105 | var site; 106 | var board; 107 | var thread; 108 | var post3; 109 | handleInput({mode: 'server', type: 'create', password: 'fugg', title: 'Server-chan'}, [null], 'hurr', 'http://localhost:3010').then(obj => { 110 | server = obj.result; 111 | console.log('Server: ' + server); 112 | return handleInput({mode: 'mod', type: 'create', password: 'fugg', title: 'Pockets the Mod', server: server}, [null], 'hurr'); 113 | }).then(obj => { 114 | mod = obj.result; 115 | console.log('Mod: ' + mod); 116 | return handleInput({mode: 'site', type: 'create', password: 'fugg', title: 'Smugchan NEXT', formats: ['png', 'jpg'], mods: [mod], server: server}, [null], 'hurr'); 117 | }).then(obj => { 118 | site = obj.result; 119 | console.log('Site: ' + site); 120 | return handleInput({mode: 'thread', type: 'create', password: 'fugg', title: 'My Shitthread', server: server}, [null], 'hurr'); 121 | }).then(obj => { 122 | thread = obj.result; 123 | console.log('Thread: ' + thread); 124 | return handleInput({mode: 'board', type: 'create', password: 'fugg', title: 'Random', formats: ['png', 'jpg'], mods: [mod], threadservers: [server], server: server}, [null], 'hurr'); 125 | }).then(obj => { 126 | board = obj.result; 127 | console.log('Board: ' + board); 128 | return handleInput({mode: 'site', type: 'add', id: site, password: 'fugg', newaddress: board, uri: 'b'}, [null], 'hurr'); 129 | }).then(obj => { 130 | console.log('Board added to site at: ' + obj.result); 131 | return handleInput({mode: 'board', type: 'add', id: board, password: 'fugg', newaddress: thread}, [null], 'hurr'); 132 | }).then(obj => { 133 | console.log('Thread added to board at: ' + obj.result); 134 | return handleInput({mode: 'thread', type: 'add', id: thread, password: 'fugg', author: 'Anonymous', email: 'sage', body: 'Babbies first shitpoast'}, [null], 'hurr'); 135 | }).then(obj => { 136 | console.log('Post added to thread at: ' + obj.result); 137 | return handleInput({mode: 'thread', type: 'add', id: thread, password: 'fugg', author: 'Anonymous', body: 'Babbies second shitpoast'}, [null], 'hurr'); 138 | }).then(obj => { 139 | console.log('Post added to thread at: ' + obj.result); 140 | return handleInput({mode: 'thread', type: 'add', id: thread, password: 'fugg', author: 'Anonymous', body: 'Babbies 3rd shitpoast'}, [null], 'hurr'); 141 | }).then(obj => { 142 | console.log('Post added to thread at: ' + obj.result); 143 | return handleInput({mode: 'thread', type: 'add', id: thread, password: 'fugg', author: 'Anonymous', body: 'Babbies 4th shitpoast'}, [null], 'hurr'); 144 | }).then(obj => { 145 | console.log('Post added to thread at: ' + obj.result); 146 | //return handleInput({mode: 'mod', type: 'add', id: mod, password: 'fugg', operation: 'remove', target: '/ipfs/Qmaddress'}, [null], 'hurr'); 147 | }).catch(console.error);*/ 148 | 149 | 150 | return; 151 | }); 152 | } 153 | 154 | module.exports.handleInput = handleInput; 155 | module.exports.setupServer = setupServer; 156 | 157 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "browser": { 9 | "request": "xhr", 10 | "image-size": "xhr", 11 | "./storage.js": "./storagebrowser.js" 12 | }, 13 | "author": "smugdev", 14 | "license": "MIT", 15 | "dependencies": { 16 | "image-size": "^0.6.1", 17 | "ipfs-api": "^14.3.5", 18 | "request": "^2.81.0", 19 | "xhr": "^2.4.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/payloads.js: -------------------------------------------------------------------------------- 1 | var sio = require('./sio.js'); 2 | var imgDims = require('image-size');//TODO presently this package doesn't work with browserify - good luck fixing that one 3 | //TODO consider instead making the client work out the dimensions 4 | 5 | function processSingleFile(fileInfo, formats, previewable){ 6 | return new Promise((resolve, reject) => { 7 | console.log(fileInfo); 8 | if (fileInfo != null && fileInfo.buffer != null){ 9 | let file = {}; 10 | file.filename = fileInfo.originalname; 11 | let filenameSplit = fileInfo.originalname.split('.'); 12 | file.extension = filenameSplit[filenameSplit.length - 1]; 13 | file.size = fileInfo.size; 14 | 15 | if (formats.indexOf(file.extension) !== -1){ 16 | sio.ipfsAddBuffer(fileInfo.buffer).then(fileHash => { 17 | return sio.ipfsNameObject(fileHash, fileInfo.originalname); 18 | }).then(fileAddress => { 19 | file.address = fileAddress; 20 | 21 | let fileDims = null; 22 | if (previewable.indexOf(file.extension) !== -1){ 23 | try { 24 | fileDims = imgDims(fileInfo.buffer); 25 | } catch (err){ 26 | reject('Getting image dims failed: ' + err); 27 | } 28 | } 29 | if (fileDims != null){ 30 | file.height = fileDims.height; 31 | file.width = fileDims.width; 32 | } 33 | 34 | resolve(file); 35 | }).catch(reject); 36 | } else { 37 | reject('Invalid filetype.'); 38 | } 39 | } else { 40 | reject('Invalid file data.'); 41 | } 42 | }); 43 | } 44 | 45 | function processFiles(filesInfo, formats, previewable){ 46 | return new Promise((resolve, reject) => { 47 | let fileProcessors = []; 48 | for (let fileInfo of filesInfo){ 49 | if (fileInfo != null){ 50 | fileProcessors.push(processSingleFile(fileInfo, formats, previewable)); 51 | } 52 | } 53 | Promise.all(fileProcessors).then(values => { //Promise.all is so fucking cool, I should've used this a long time ago 54 | resolve(values); 55 | }).catch(reject); 56 | }); 57 | } 58 | 59 | function getThreadPayload(msg, filesInfo, formats, previewable){ 60 | return new Promise((resolve, reject) => { 61 | let payload = {author: '', email: '', subject: '', body: ''}; 62 | if (msg.author) { 63 | payload.author = msg.author; 64 | } 65 | if (msg.email) { 66 | payload.email = msg.email; 67 | } 68 | if (msg.subject) { 69 | payload.subject = msg.subject; 70 | } 71 | if (msg.body) { 72 | payload.body = msg.body; 73 | } 74 | 75 | processFiles(filesInfo, formats, previewable).then(files => { 76 | payload.files = files; 77 | return sio.ipfsAddObject(payload); 78 | }).then(payloadAddress => { 79 | resolve({data: {post: {address: payloadAddress}}}); 80 | }).catch(reject); 81 | }); 82 | } 83 | 84 | function getBoardPayload(msg){ 85 | return new Promise((resolve, reject) => { 86 | let payload = {data: {thread: {address: ''}}}; 87 | if (msg.newaddress) { 88 | payload.data.thread.address = msg.newaddress; 89 | } //TODO verify this is a valid IPNS name, and perhaps that it points to a valid thread wrapper? or doing it client side would work too probably 90 | 91 | resolve(payload); 92 | //sio.ipfsAddObject(payload).then(payloadAddress => { 93 | //resolve(payloadAddress); 94 | //}).catch(reject); 95 | }); 96 | } 97 | 98 | function getSitePayload(msg){ 99 | return new Promise((resolve, reject) => { 100 | let payload = {data: {}}; 101 | if (msg.newaddress) { 102 | payload.data.address = msg.newaddress; 103 | } else { 104 | reject('Missing address of board to attach.'); 105 | } 106 | 107 | if (msg.uri) { 108 | payload.data.uri = msg.uri; 109 | } else { 110 | reject('Missing uri to assign board to.'); 111 | } 112 | 113 | resolve(payload); 114 | //sio.ipfsAddObject(payload).then(payloadAddress => { 115 | // resolve(payloadAddress); 116 | //}).catch(reject); 117 | }); 118 | } 119 | 120 | function getModPayload(msg){ 121 | return new Promise((resolve, reject) => { 122 | let payload = {data: {}}; 123 | if (msg.target) { 124 | payload.data.target = msg.target; 125 | } else { 126 | reject('Missing address of post to apply action to.'); 127 | } 128 | 129 | if (msg.operation) { 130 | payload.data.operation = msg.operation; 131 | } else { 132 | reject('Missing operation type.'); 133 | } 134 | 135 | if (msg.content) { 136 | payload.data.content = msg.content; 137 | } 138 | 139 | resolve(payload); 140 | //sio.ipfsAddBuffer(JSON.stringify(payload)).then(payloadAddress => { 141 | // resolve(payloadAddress); 142 | //}).catch(reject); 143 | }); 144 | } 145 | 146 | 147 | module.exports.getThreadPayload = getThreadPayload; 148 | module.exports.getBoardPayload = getBoardPayload; 149 | module.exports.getSitePayload = getSitePayload; 150 | module.exports.getModPayload = getModPayload; 151 | -------------------------------------------------------------------------------- /common/settings.js: -------------------------------------------------------------------------------- 1 | /* Server public IP address */ 2 | module.exports.serverAddress = 'http://localhost'; 3 | 4 | /* Server default password (temporary arrangement) */ 5 | module.exports.password = 'myverysecurepassword'; 6 | 7 | -------------------------------------------------------------------------------- /common/sio.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var ipfsAPI = require('ipfs-api'); 3 | var ipfs = ipfsAPI('localhost', '5001', {protocol: 'http'}); 4 | 5 | var nameservers = []; 6 | nameservers.push(require('./settings.js').serverAddress + ':3005'); 7 | var sitePassword = require('./settings.js').password; 8 | 9 | var util = require('./util.js'); 10 | 11 | function genKey(keyName){ 12 | return ipfs.key.gen(keyName, {type: 'rsa', size: '2048'}); 13 | } 14 | 15 | function getKeys(){ 16 | return ipfs.key.list(); 17 | } 18 | 19 | function ipfsAddBuffer(buffer){ 20 | return ipfs.util.addFromStream(buffer).then(result => { 21 | return result[result.length - 1].hash; 22 | }); 23 | } 24 | 25 | function ipfsAddObject(object){ 26 | return ipfsAddBuffer(Buffer.from(JSON.stringify(object), 'utf8')).then(hash => { 27 | return '/ipfs/' + hash; 28 | }); 29 | } 30 | 31 | function ipfsNameObject(hash, filename){ 32 | return ipfs.object.new('unixfs-dir').then(node => { 33 | return ipfs.object.patch.addLink(node.multihash, { 34 | name: filename, 35 | multihash: hash 36 | }); 37 | }).then(result => { 38 | return '/ipfs/' + result._json.multihash + '/' + filename; 39 | }); 40 | } 41 | //ipfs.object.new('unixfs-dir').then(node => {return ipfs.object.patch.addLink(node.multihash, {name: 'hurr.jpg', multihash: 'QmfT7z9atYSk1ymQjMMBtDbDv8prj67roAkFzn8MAeT2BA'})}).then(console.log).catch(console.error) 42 | 43 | function ipfsPublishToKey(address, keyName){ 44 | return ipfs.name.publish(address, {key: keyName}); 45 | } 46 | 47 | function publishToNameservers(id, association, password){ 48 | return new Promise((resolve, reject) => { 49 | let promises = []; 50 | let jsonObj = { 51 | address: id, 52 | association: association, 53 | password: password 54 | }; 55 | for (let server of nameservers){ 56 | promises.push(sendPost(server, jsonObj)); 57 | } 58 | Promise.all(promises).then(() => { 59 | resolve('Published to all name servers.'); 60 | }).catch(reject); 61 | }); 62 | } 63 | 64 | function ipfsPublish(address, keyName, pubkey){ 65 | console.log('Publishing ' + address + ' to ' + keyName + ' (' + pubkey + ')'); 66 | 67 | publishToNameservers(pubkey, address, sitePassword).then(resp => console.log).catch(err => console.error); 68 | 69 | ipfsPublishToKey(address, keyName).then(resp => console.log).catch(err => console.error); 70 | } 71 | 72 | function resolveFromSingleNameserver(nameserver, address){ 73 | return new Promise((resolve, reject) => { 74 | let requestAddress = nameserver + '?address=' + address; 75 | loadAddress(requestAddress).then(resolve).catch(reject); 76 | }); 77 | } 78 | 79 | function resolveFromNameservers(nameservers, address){ 80 | return new Promise((resolve, reject) => { 81 | if (nameservers.length < 1) { 82 | reject('No nameservers.'); 83 | } 84 | let promises = []; 85 | for (let nameserver of nameservers){ 86 | promises.push(resolveFromSingleNameserver(nameserver, address).then(resolve)); 87 | } 88 | Promise.all(promises).catch(reject); 89 | // TODO The logic is supposed to be be "return the first nameserver that resolves, and if none of them do then reject" - need to check the impl. here is right 90 | // TODO it's not right, promise.all will reject as soon as one of these fails. Fuck. Why can't Promise.race do normal shit 91 | }); 92 | } 93 | 94 | function ipfsResolveName(address){ 95 | return new Promise((resolve, reject) => { 96 | ipfs.name.resolve(address, (err, resolvedName) => { 97 | if (err) { 98 | reject(err); 99 | } 100 | resolve(resolvedName.Path); 101 | }); 102 | }); 103 | } 104 | 105 | function resolveName(address){ 106 | return new Promise((resolve, reject) => { 107 | var addressSplit = address.split('/');// TODO ensure we can assume that the addresses will already be correct 108 | var addressActual = '/' + addressSplit[addressSplit.length - 2] + '/' + addressSplit[addressSplit.length - 1]; 109 | if (addressSplit[addressSplit.length - 2] === 'ipns'){ 110 | resolveFromNameservers(nameservers, addressActual).then(resolve).catch(err => { 111 | console.error(err); 112 | ipfsResolveName(addressActual).then(resolve).catch(reject); 113 | }); 114 | } else { 115 | resolve(addressActual); 116 | } 117 | }); 118 | } 119 | 120 | function ipfsGetJsonObject(address){ 121 | return new Promise((resolve, reject) => { 122 | ipfs.files.cat(util.hashFromAddress(address)).then(stream => { 123 | let result = ''; 124 | stream.on('data', (chunk) => { 125 | result += chunk; 126 | }); 127 | stream.on('end', () => { 128 | resolve(JSON.parse(result)); 129 | }); 130 | }).catch(reject); 131 | }); 132 | } 133 | 134 | function getJsonObject(address){ 135 | return resolveName(address).then(addressActual => { 136 | return ipfsGetJsonObject(addressActual); 137 | }); 138 | } 139 | 140 | function loadAddress(address){ 141 | return new Promise((resolve, reject) => { 142 | request({ 143 | uri: address, 144 | headers: { 145 | 'Content-Type': 'application/json' 146 | } 147 | }, (err, resp, body) => { 148 | if (resp != null && resp.statusCode === 200){ 149 | if (resp.body != null){ 150 | resolve(resp.body); 151 | } else { 152 | reject('Invalid response: ' + resp); 153 | } 154 | } else { 155 | reject(err); 156 | } 157 | }); 158 | }); 159 | 160 | } 161 | 162 | function sendPost(server, jsonObj){ 163 | return new Promise((resolve, reject) => { 164 | request.post({url: server, formData: jsonObj}, (err, httpResponse, body) => { 165 | if (err) { 166 | reject(err); 167 | } 168 | resolve(body); 169 | }); 170 | }); 171 | } 172 | 173 | 174 | module.exports.genKey = genKey; 175 | module.exports.getKeys = getKeys; 176 | module.exports.ipfsAddBuffer = ipfsAddBuffer; 177 | module.exports.ipfsAddObject = ipfsAddObject; 178 | module.exports.ipfsNameObject = ipfsNameObject; 179 | module.exports.ipfsPublish = ipfsPublish; 180 | module.exports.resolveName = resolveName; 181 | module.exports.getJsonObject = getJsonObject; 182 | module.exports.sendPost = sendPost; 183 | 184 | -------------------------------------------------------------------------------- /common/slog.js: -------------------------------------------------------------------------------- 1 | var sio = require('./sio.js'); 2 | 3 | function calculateSLogPayloads(sLog){ 4 | let i, j; 5 | 6 | for (i = sLog.length - 1; i >= 0; i--){ 7 | if (sLog[i].data.operation === 'remove' && sLog[i].data.target != null && !sLog[i].removed){ 8 | for (j = i - 1; j >= 0; j--){ 9 | if (sLog[j].address === sLog[i].data.target){ 10 | sLog[j].removed = true; 11 | break; 12 | } 13 | } 14 | } 15 | } 16 | 17 | //TODO expand keyframes here 18 | } 19 | 20 | function subtractSLog(sLog, deletionSLog){ 21 | let targets = []; 22 | if (deletionSLog) { 23 | for (let item of deletionSLog){ 24 | if (!item.removed && item.payload && item.payload.data && item.payload.data.target){//TODO run through getSLogObj 25 | targets.push(item.payload.data.target); 26 | } 27 | } 28 | } 29 | 30 | for (let item of sLog){ 31 | if (targets.indexOf(sLog.address) !== -1){ 32 | item.removed = true; 33 | } 34 | } 35 | } 36 | 37 | function getLatestSeqno(sLog){ 38 | for (let i = sLog.length - 1; i >= 0; i--){ 39 | if (sLog[i].seqno != null){ 40 | return sLog[i].seqno; 41 | } 42 | } 43 | return 0; 44 | } 45 | 46 | //gets data from object, visiting the address if necessary 47 | function getSLogObj(obj){ 48 | return new Promise((resolve, reject) => { 49 | if (obj.data != null){ 50 | resolve(obj); 51 | } else if (obj.address == null){ 52 | reject('Malformed object: ' + obj);//obj can have a .address, but can also have a .data for speed. obj.address should point to the exact same JSON object as obj.data if it exists. 53 | } else { 54 | sio.getJsonObject(obj.address).then(result => { 55 | obj.data = result; 56 | resolve(obj); 57 | }).catch(reject); 58 | } 59 | }); 60 | } 61 | 62 | function recursiveLoad(runningSLog, sLogLastGoodNext, insertAfter, depth, onCompletion, onFail){ 63 | return function(sLogEntry){ 64 | //sLogEntry.address = objAddress; 65 | if (runningSLog == null) { 66 | runningSLog = []; 67 | } 68 | if (sLogEntry.data.next != null && sLogEntry.data.next.address != null && sLogLastGoodNext === sLogEntry.data.next.address){ 69 | //we know that if we see the same next value, we've already seen this node since this is an append-only linked list built on a merkel-dag 70 | if (onCompletion != null) { 71 | onCompletion(runningSLog); 72 | } 73 | } else { 74 | if (insertAfter === -1){ 75 | runningSLog.unshift(sLogEntry); 76 | } else if (runningSLog.length === insertAfter + 1){ 77 | runningSLog.push(sLogEntry); 78 | } else { 79 | runningSLog.splice(insertAfter + 1, 0, sLogEntry); 80 | } 81 | 82 | //recurse until we hit the list tail or we hit a recursion depth limit 83 | if (sLogEntry.data.next != null && (depth === -1 || depth > 0)){ 84 | getSLogObj(sLogEntry.data.next).then(recursiveLoad(runningSLog, sLogLastGoodNext, insertAfter, depth === -1 ? depth : --depth, onCompletion, onFail)).catch(onFail); 85 | } else { 86 | if (onCompletion != null) { 87 | onCompletion(runningSLog); 88 | } 89 | } 90 | } 91 | }; 92 | } 93 | 94 | function updateSLog(sLog, sLogAddress, depth){ 95 | return new Promise((resolve, reject) => { 96 | var sLogLastGood; 97 | var insertAfter; 98 | if (sLog == null){ 99 | sLogLastGood = 'default'; 100 | insertAfter = -1; 101 | } else { 102 | if (sLog[sLog.length - 1].data.next) { 103 | sLogLastGood = sLog[sLog.length - 1].data.next.address; 104 | } else { 105 | sLogLastGood = 'default'; 106 | } 107 | insertAfter = sLog.length - 1; 108 | } 109 | 110 | getSLogObj(sLogAddress).then(recursiveLoad(sLog, sLogLastGood, insertAfter, depth, resolve, reject)).catch(reject); 111 | }); 112 | } 113 | 114 | 115 | function newSLogEntry(operation){ 116 | var result = { 117 | timestamp: Date.now(), 118 | next: null, 119 | operation: operation, 120 | }; 121 | 122 | return result; 123 | } 124 | 125 | module.exports.calculateSLogPayloads = calculateSLogPayloads; 126 | module.exports.subtractSLog = subtractSLog; 127 | module.exports.getSLogObj = getSLogObj; 128 | module.exports.updateSLog = updateSLog; 129 | module.exports.getLatestSeqno = getLatestSeqno; 130 | module.exports.newSLogEntry = newSLogEntry; 131 | 132 | -------------------------------------------------------------------------------- /common/storage.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var wrappersFile = 'wrappers.json'; 3 | 4 | function saveObject(filename, obj){//TODO deprecate this 5 | return new Promise((resolve, reject) => { 6 | fs.writeFile(filename, JSON.stringify(obj), (err) => { 7 | if (err) { 8 | reject(err); 9 | } 10 | resolve(); 11 | }); 12 | }); 13 | } 14 | 15 | function saveWrappers(wrappers){ 16 | //return new Promise((resolve, reject) => { 17 | return saveObject(wrappersFile, wrappers);//.then(resolve).catch(reject); 18 | //}); 19 | } 20 | 21 | function loadObjectIfExists(filename){//TODO deprecate this, use something also browser-compatible 22 | return new Promise((resolve, reject) => { 23 | fs.readFile(filename, (err, data) => { 24 | if (err) { 25 | reject(err); 26 | } 27 | resolve(data); 28 | }); 29 | }); 30 | } 31 | 32 | function getWrappers(){ 33 | return new Promise((resolve, reject) => { 34 | loadObjectIfExists(wrappersFile).then((data) => { 35 | resolve(JSON.parse(data)); 36 | }).catch(() => { 37 | resolve({}); 38 | }); 39 | }); 40 | } 41 | module.exports.saveObject = saveObject; 42 | module.exports.saveWrappers = saveWrappers; 43 | module.exports.loadObjectIfExists = loadObjectIfExists; 44 | module.exports.getWrappers = getWrappers; 45 | -------------------------------------------------------------------------------- /common/storagebrowser.js: -------------------------------------------------------------------------------- 1 | function getCookie(cname) { 2 | var name = cname + '='; 3 | var ca = document.cookie.split(';'); 4 | for(var i = 0; i < ca.length; i++) { 5 | var c = ca[i]; 6 | while (c.charAt(0) === ' ') { 7 | c = c.substring(1); 8 | } 9 | if (c.indexOf(name) === 0) { 10 | return c.substring(name.length,c.length); 11 | } 12 | } 13 | return ''; 14 | } 15 | 16 | function saveWrappers(wrappers){ 17 | document.cookie = 'wrappers=' + JSON.stringify(wrappers) + '; path=/'; 18 | } 19 | 20 | function getWrappers(){ 21 | let currentData = getCookie('wrappers'); 22 | if (currentData !== ''){ 23 | return JSON.parse(currentData); 24 | } 25 | return {}; 26 | } 27 | 28 | module.exports.saveWrappers = saveWrappers; 29 | module.exports.getWrappers = getWrappers; 30 | -------------------------------------------------------------------------------- /common/util.js: -------------------------------------------------------------------------------- 1 | function propertyExists(obj, prop) { 2 | var parts = prop.split('.'); 3 | for(var i = 0, l = parts.length; i < l; i++) { 4 | var part = parts[i]; 5 | if(obj !== null && typeof obj === 'object' && part in obj) { 6 | obj = obj[part]; 7 | } else { 8 | return false; 9 | } 10 | } 11 | return true; 12 | } 13 | 14 | function hashFromAddress(address){ 15 | var hash = address.split('/'); 16 | for (let i = 0; i < hash.length; i++){ 17 | if (hash[i] === 'ipfs' || hash[i] === 'ipns'){ 18 | return hash[i+1]; 19 | } 20 | } 21 | return hash[hash.length - 1]; 22 | } 23 | 24 | function rebuildKeys(rawKeys, daemonKeys){ 25 | for (let keyId of Object.keys(rawKeys)){ 26 | rawKeys[keyId].last = {address: rawKeys[keyId].last}; 27 | } 28 | for (let key of daemonKeys.Keys){ 29 | if (rawKeys['/ipns/' + key.Id] != null){ 30 | rawKeys['/ipns/' + key.Id].name = key.Name; 31 | } 32 | } 33 | return rawKeys; 34 | } 35 | 36 | function prepareStorageKeys(rawKeys){ 37 | var storageKeys = {}; 38 | for (let keyId of Object.keys(rawKeys)){ 39 | storageKeys[keyId] = {last: rawKeys[keyId].last.address, password: rawKeys[keyId].password}; 40 | } 41 | return storageKeys; 42 | } 43 | 44 | function processQueue(obj, myFunc){ 45 | 46 | if (!obj.funcQueue) { 47 | obj.funcQueue = []; 48 | } 49 | 50 | //myFunc must take (callback) 51 | if (myFunc) { 52 | obj.funcQueue.push({func: myFunc, running: false}); 53 | } 54 | 55 | let somethingRunning = false; 56 | for (let item of obj.funcQueue) { 57 | if (item.running === true) { 58 | somethingRunning = true; 59 | } 60 | } 61 | 62 | if (!somethingRunning && obj.funcQueue.length > 0){ 63 | obj.funcQueue[0].running = true; 64 | obj.funcQueue[0].func(() => { 65 | obj.funcQueue.shift(); 66 | processQueue(obj); 67 | }); 68 | } 69 | } 70 | 71 | function acquireLock(obj){ 72 | return new Promise((resolve, reject) => { 73 | processQueue(obj, (releaseLock) => { 74 | resolve(releaseLock); 75 | }); 76 | }); 77 | } 78 | 79 | module.exports.propertyExists = propertyExists; 80 | module.exports.hashFromAddress = hashFromAddress; 81 | module.exports.rebuildKeys = rebuildKeys; 82 | module.exports.prepareStorageKeys = prepareStorageKeys; 83 | //module.exports.processQueue = processQueue; 84 | module.exports.acquireLock = acquireLock; 85 | -------------------------------------------------------------------------------- /common/wrapperfuncs.js: -------------------------------------------------------------------------------- 1 | var sio = require('./sio.js'); 2 | var slog = require('./slog.js'); 3 | var util = require('./util.js'); 4 | var payloads = require('./payloads.js'); 5 | 6 | function getBlankWrapper(keyName){ 7 | return {seqno: 0, name: keyName, last: {data: {info: {}, head: null}}}; 8 | } 9 | 10 | function fillWrapper(wrapper, msg, serverAddress){ 11 | wrapper.last.data.info.mode = msg.mode; 12 | wrapper.password = msg.password; 13 | 14 | if (msg.title && msg.mode !== 'thread') { 15 | wrapper.last.data.info.title = msg.title; 16 | } 17 | if (msg.formats) { 18 | wrapper.last.data.info.formats = Object.prototype.toString.call(msg.formats) === '[object Array]' ? msg.formats : [msg.formats]; 19 | } 20 | if (msg.mods) { 21 | wrapper.last.data.info.mods = Object.prototype.toString.call(msg.mods) === '[object Array]' ? msg.mods : [msg.mods]; 22 | } 23 | if (msg.threadservers) { 24 | wrapper.last.data.info.threadservers = Object.prototype.toString.call(msg.threadservers) === '[object Array]' ? msg.threadservers : [msg.threadservers]; 25 | } 26 | if (msg.server){ 27 | wrapper.last.data.info.server = msg.server; 28 | } else if (serverAddress){ 29 | wrapper.last.data.info.serverconn = {ip: serverAddress}; 30 | } 31 | 32 | } 33 | 34 | function loadWrapper(wrapper){ 35 | return new Promise((resolve, reject) => { 36 | slog.getSLogObj(wrapper.last).then((result) => { 37 | wrapper.last = result; 38 | if (wrapper.last.data.head == null){ 39 | return null; 40 | } else {//if (wrapper.sLog == null){ 41 | return slog.updateSLog(wrapper.sLog, wrapper.last.data.head, -1); 42 | }// else { 43 | // return wrapper.sLog; 44 | //} 45 | }).then(sLog => { 46 | wrapper.sLog = sLog; 47 | if (wrapper.seqno == null && wrapper.sLog != null) { 48 | wrapper.seqno = slog.getLatestSeqno(wrapper.sLog); 49 | } 50 | resolve(wrapper); 51 | }).catch(reject); 52 | }); 53 | } 54 | 55 | //TODO think about merging this into loadWrapper 56 | function pullWrapperStub(wrapperId, wrappers){ 57 | return sio.resolveName(wrapperId).then(address => { 58 | if (wrappers[wrapperId] == null) { 59 | wrappers[wrapperId] = {last: {}}; 60 | } 61 | wrappers[wrapperId].last.data = null; 62 | wrappers[wrapperId].last.address = address; 63 | return slog.getSLogObj(wrappers[wrapperId].last); 64 | }); 65 | } 66 | 67 | function getServerConn(serverId, wrappers){ 68 | //return new Promise((resolve, reject) => { 69 | let promise = null; 70 | if (wrappers[serverId] == null){ 71 | promise = pullWrapperStub(serverId, wrappers); 72 | } 73 | return Promise.resolve(promise).then(() => { 74 | if (!util.propertyExists(wrappers[serverId], 'last.data.info.serverconn') || !wrappers[serverId].last.data.info.mode === 'server') { 75 | if (util.propertyExists(wrappers[serverId], 'last.data.info.server')){ 76 | return getServerConn(wrappers[serverId].last.data.info.server, wrappers); 77 | } else { 78 | return Promise.reject('Not a valid server object.'); 79 | } 80 | } else { 81 | return wrappers[serverId].last.data.info.serverconn; 82 | } 83 | }); 84 | //}); 85 | } 86 | 87 | function collectMods(mods, wrappers){ 88 | let modsPending = []; 89 | if (mods != null){ 90 | for (let mod of mods){ 91 | modsPending.push( 92 | pullWrapperStub(mod, wrappers).then(() => { 93 | return loadWrapper(wrappers[mod]); 94 | }).then(() => { 95 | return slog.calculateSLogPayloads(wrappers[mod]); 96 | }) 97 | ); 98 | } 99 | } 100 | return modsPending; 101 | } 102 | 103 | function createObj(msg, wrappers, serverAddress){ 104 | return new Promise((resolve, reject) => { 105 | let keyName = 'smug-' + Math.random().toString(36).substring(2); 106 | let newWrapper = getBlankWrapper(keyName); 107 | fillWrapper(newWrapper, msg, serverAddress); 108 | newWrapper.name = keyName; 109 | 110 | var key; 111 | sio.genKey(keyName).then(newKey => { 112 | key = '/ipns/' + newKey.Id; 113 | wrappers[key] = newWrapper; 114 | return sio.ipfsAddObject(newWrapper.last.data); 115 | }).then(newAddress => { 116 | wrappers[key].last.address = newAddress; 117 | return sio.ipfsPublish(wrappers[key].last.address, wrappers[key].name, key); 118 | }).then(() => { 119 | resolve({result: key}); 120 | }).catch((err) => { 121 | reject({status: 500, result: err}); 122 | }); 123 | }); 124 | } 125 | 126 | function addToObj(msg, wrappers, filesInfo, formats, previewable){ 127 | return new Promise((resolve, reject) => { 128 | if (msg.id == null || msg.id === '' || wrappers[msg.id] == null){ 129 | reject({status: 404, result: 'No object with that id found: ' + msg.id}); 130 | 131 | //TODO verify that the selected wrapper actually uses the mode in question 132 | //wrappers[id].last.data.mode 133 | } else { 134 | let getPayload; 135 | switch(msg.mode){ 136 | case 'thread': 137 | getPayload = payloads.getThreadPayload(msg, filesInfo, formats, previewable); 138 | break; 139 | case 'board': 140 | getPayload = payloads.getBoardPayload(msg); 141 | break; 142 | case 'site': 143 | getPayload = payloads.getSitePayload(msg); 144 | break; 145 | case 'mod': 146 | getPayload = payloads.getModPayload(msg); 147 | break; 148 | default: 149 | reject({status: 500, result: 'Unsupported operation.'}); 150 | } 151 | 152 | let newEntry; 153 | let releaser; 154 | //let seqno; 155 | getPayload.then(payload => { 156 | newEntry = slog.newSLogEntry('add'); 157 | newEntry.payload = payload; 158 | return util.acquireLock(wrappers[msg.id]); 159 | }).then(releaseLock => { 160 | releaser = releaseLock; 161 | return loadWrapper(wrappers[msg.id]); 162 | }).then(() => { 163 | newEntry.seqno = ++wrappers[msg.id].seqno; 164 | if (wrappers[msg.id].last.data.head) { 165 | newEntry.next = {address: wrappers[msg.id].last.data.head.address}; 166 | } 167 | return sio.ipfsAddObject(newEntry); 168 | }).then(newEntryAddress => { 169 | if (wrappers[msg.id].last.data.info.mode === 'thread' && wrappers[msg.id].last.data.info.op == null){ 170 | wrappers[msg.id].last.data.info.op = {address: newEntryAddress}; 171 | } 172 | wrappers[msg.id].last.data.head = {address: newEntryAddress}; 173 | return sio.ipfsAddObject(wrappers[msg.id].last.data); 174 | }).then(newAddress => { 175 | wrappers[msg.id].last.address = newAddress; 176 | //seqno = wrappers[msg.id].seqno; 177 | releaser(); 178 | releaser = null; 179 | return sio.ipfsPublish(wrappers[msg.id].last.address, wrappers[msg.id].name, msg.id); 180 | }).then(() => { 181 | resolve({result: '' + wrappers[msg.id].seqno}); 182 | }).catch(err => { 183 | if (releaser) { 184 | releaser(); 185 | } 186 | reject({status: 500, result: err}); 187 | }); 188 | 189 | } 190 | }); 191 | } 192 | 193 | function removeFromObj(msg, wrappers){ 194 | return new Promise((resolve, reject) => { 195 | if (msg.id == null || msg.id === '' || wrappers[msg.id] == null){ 196 | reject({status: 404, result: 'No object with that id found: ' + msg.id}); 197 | 198 | //TODO verify that the selected wrapper actually uses the mode in question 199 | //wrappers[id].last.data.mode 200 | //or perhaps don't need to bother here? 201 | } else { 202 | let newEntry; 203 | let releaser; 204 | 205 | util.acquireLock(wrappers[msg.id]).then(releaseLock => { 206 | releaser = releaseLock; 207 | return loadWrapper(wrappers[msg.id]); 208 | }).then(() => { 209 | newEntry = slog.newSLogEntry('remove'); 210 | newEntry.target = msg.target; 211 | if (wrappers[msg.id].last.data.head) { 212 | newEntry.next = {address: wrappers[msg.id].last.data.head.address}; 213 | } 214 | return sio.ipfsAddObject(newEntry); 215 | }).then(newEntryAddress => { 216 | wrappers[msg.id].last.data.head = {address: newEntryAddress}; 217 | return sio.ipfsAddObject(wrappers[msg.id].last.data); 218 | }).then(newAddress => { 219 | wrappers[msg.id].last.address = newAddress; 220 | releaser(); 221 | releaser = null; 222 | return sio.ipfsPublish(wrappers[msg.id].last.address, wrappers[msg.id].name, msg.id); 223 | }).then(() => { 224 | resolve({result: 'Removal successful.'}); 225 | }).catch(err => { 226 | if (releaser) { 227 | releaser(); 228 | } 229 | reject({status: 500, result: err}); 230 | }); 231 | } 232 | }); 233 | } 234 | 235 | function setObj(msg, wrappers, serverAddress){//TODO 236 | return new Promise((resolve, reject) => { 237 | if (msg.id == null || msg.id === '' || wrappers[msg.id] == null){ 238 | reject({status: 404, result: 'No object with that id found: ' + msg.id}); 239 | 240 | //TODO verify that the selected wrapper actually uses the mode in question 241 | //wrappers[id].last.data.mode 242 | } else { 243 | //if msg.lastdata 244 | //JSON.parse(msg.lastdata) 245 | //wrappers[msg.id].last = {data: msg.lastdata} 246 | //recalculate wrappers[msg.id].last.address 247 | //publish wrappers[msg.id].last.address to wrappers[msg.id].name (msg.id) 248 | //wrappers[msg.id].seqno = null 249 | //wrappers[msg.id].sLog = null 250 | } 251 | }); 252 | } 253 | 254 | function deleteObj(msg, wrappers){ 255 | return new Promise((resolve, reject) => { 256 | if (msg.id == null || msg.id === '' || wrappers[msg.id] == null){ 257 | reject({status: 404, result: 'No object with that id found: ' + msg.id}); 258 | } else { 259 | wrappers[msg.id] = null; //well that was easy 260 | resolve({result: msg.id + ' deleted.'}); 261 | //TODO consider purging all object content from ipfs... but need to think about what if some content appears in multiple wrappers 262 | } 263 | }); 264 | } 265 | 266 | module.exports.loadWrapper = loadWrapper; 267 | module.exports.pullWrapperStub = pullWrapperStub; 268 | module.exports.getServerConn = getServerConn; 269 | module.exports.collectMods = collectMods; 270 | module.exports.createObj = createObj; 271 | module.exports.addToObj = addToObj; 272 | module.exports.removeFromObj = removeFromObj; 273 | module.exports.setObj = setObj; 274 | module.exports.deleteObj = deleteObj; 275 | 276 | -------------------------------------------------------------------------------- /install_all.sh: -------------------------------------------------------------------------------- 1 | export IPFS_PATH=./.ipfs 2 | ipfs init 3 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]" 4 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\"true\"]" 5 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\"PUT\", \"POST\", \"GET\"]" 6 | 7 | cd server 8 | npm install 9 | cd ../common 10 | npm install 11 | cd ../nameserver 12 | npm install 13 | -------------------------------------------------------------------------------- /kill_all.sh: -------------------------------------------------------------------------------- 1 | pkill -F .ipfs/ipfs.pid 2 | pkill -F server/server.pid 3 | pkill -F nameserver/server.pid 4 | 5 | ./clean_pids.sh 6 | -------------------------------------------------------------------------------- /nameserver/namemanager.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var cors = require('cors'); 3 | var multer = require('multer'); 4 | //var pem = require('pem'); 5 | //var https = require('https'); 6 | //var fs = require('fs'); 7 | 8 | var upload = multer(); 9 | //var sio = require('../common/sio.js'); 10 | var storage = require('../common/storage.js'); 11 | 12 | var app = express(); 13 | 14 | app.use(cors()); 15 | 16 | var sitePassword = require('../common/settings.js').password; 17 | var port = 3005; //0 makes the program just pick something free 18 | 19 | var bindings; 20 | 21 | app.post('/', upload.fields([]), function (req, res) {//TODO get rid of the damn file uploads 22 | console.log('POST received: ' + JSON.stringify(req.body)); 23 | 24 | if (req.body != null){ 25 | if (req.body.address != null && req.body.association != null && req.body.password != null){ 26 | if (req.body.password === sitePassword){ 27 | bindings[req.body.address] = req.body.association; 28 | storage.saveObject('bindings.json', bindings).then(console.log).catch(console.error); 29 | res.send('Name successfully registered.'); 30 | } else { 31 | res.status(400).send('Wrong password.'); 32 | } 33 | } else { 34 | res.status(400).send('Malformed POST (wrong fields).'); 35 | } 36 | } else { 37 | res.status(400).send('Malformed POST (missing body).'); 38 | } 39 | }); 40 | 41 | app.get('/', upload.fields([]), function (req, res) { 42 | console.log('GET received: ' + JSON.stringify(req.query)); 43 | //console.log(req); 44 | //res.set('Access-Control-Allow-Origin', '*'); 45 | 46 | if (req.query != null && req.query.address != null){ 47 | if (bindings[req.query.address] != null){ 48 | res.send(bindings[req.query.address]); 49 | } else { 50 | res.status(404).send('Unknown name.'); 51 | } 52 | } else { 53 | res.status(404).send('Malformed request.'); 54 | } 55 | }); 56 | 57 | bindings = storage.loadObjectIfExists('bindings.json').then(result => { 58 | bindings = result; 59 | }).catch(() => { 60 | bindings = {}; 61 | }); 62 | 63 | var listener = app.listen(port, function(){ 64 | console.log('Now listening on port ' + listener.address().port); 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /nameserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "namemanager", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "namemanager.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "BSD-2-Clause", 11 | "dependencies": { 12 | "cors": "^2.8.0", 13 | "express": "~4.14.0", 14 | "multer": "^1.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /populate_boards.sh: -------------------------------------------------------------------------------- 1 | echo "Populating boards..." 2 | 3 | #create board mods 4 | amod=$(node scripts/createmod.js $1) 5 | bmod=$(node scripts/createmod.js $1) 6 | cmod=$(node scripts/createmod.js $1) 7 | devmod=$(node scripts/createmod.js $1) 8 | polmod=$(node scripts/createmod.js $1) 9 | techmod=$(node scripts/createmod.js $1) 10 | vmod=$(node scripts/createmod.js $1) 11 | 12 | #create boards 13 | aboard=$(node scripts/createboard.js $1 $amod $1 "Animu & Mango") 14 | bboard=$(node scripts/createboard.js $1 $bmod $1 "Random") 15 | cboard=$(node scripts/createboard.js $1 $cmod $1 "Anime/Cute") 16 | devboard=$(node scripts/createboard.js $1 $devmod $1 "Smugboard Development") 17 | polboard=$(node scripts/createboard.js $1 $polmod $1 "Politically Incorrect") 18 | techboard=$(node scripts/createboard.js $1 $techmod $1 "Technology") 19 | vboard=$(node scripts/createboard.js $1 $vmod $1 "Video Games") 20 | 21 | #add boards to site 22 | ares=$(node scripts/addboardtosite.js $2 $aboard a) 23 | bres=$(node scripts/addboardtosite.js $2 $bboard b) 24 | cres=$(node scripts/addboardtosite.js $2 $cboard c) 25 | devres=$(node scripts/addboardtosite.js $2 $devboard dev) 26 | polres=$(node scripts/addboardtosite.js $2 $polboard pol) 27 | techres=$(node scripts/addboardtosite.js $2 $techboard tech) 28 | vres=$(node scripts/addboardtosite.js $2 $vboard v) 29 | 30 | echo "Done" 31 | -------------------------------------------------------------------------------- /scripts/addboardtosite.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 5){ 7 | console.error('node addboardtosite.js '); 8 | process.exit(); 9 | } 10 | 11 | var site = process.argv[2]; 12 | var board = process.argv[3]; 13 | var uri = process.argv[4]; 14 | 15 | sio.sendPost(serverAddress + ':' + port, { 16 | mode: 'site', 17 | type: 'add', 18 | password: password, 19 | id: site, 20 | newaddress: board, 21 | uri: uri 22 | }).then(console.log).catch(console.error); 23 | 24 | -------------------------------------------------------------------------------- /scripts/addthreadtoboard.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 4){ 7 | console.error('node addthreadtoboard.js '); 8 | process.exit(); 9 | } 10 | 11 | var board = process.argv[2]; 12 | var thread = process.argv[3]; 13 | 14 | sio.sendPost(serverAddress + ':' + port, { 15 | mode: 'board', 16 | type: 'add', 17 | password: password, 18 | id: board, 19 | newaddress: thread 20 | }).then(console.log).catch(console.error); 21 | 22 | -------------------------------------------------------------------------------- /scripts/createboard.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 6){ 7 | console.error('node createboard.js '); 8 | process.exit(); 9 | } 10 | 11 | var server = process.argv[2]; 12 | var mods = process.argv[3]; 13 | var threadServer = process.argv[4]; 14 | var title = process.argv[5]; 15 | 16 | sio.sendPost(serverAddress + ':' + port, { 17 | mode: 'board', 18 | type: 'create', 19 | password: password, 20 | title: title, 21 | formats: ['png', 'jpg'], 22 | mods: [mods], 23 | threadservers: [threadServer], 24 | server: server 25 | }).then(console.log).catch(console.error); 26 | 27 | -------------------------------------------------------------------------------- /scripts/createmod.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 3){ 7 | console.error('node createmod.js <SERVER>'); 8 | process.exit(); 9 | } 10 | 11 | var server = process.argv[2]; 12 | 13 | sio.sendPost(serverAddress + ':' + port, { 14 | mode: 'mod', 15 | type: 'create', 16 | password: password, 17 | title: 'Pockets the Mod', 18 | server: server 19 | }).then(console.log).catch(console.error); 20 | -------------------------------------------------------------------------------- /scripts/createserver.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | sio.sendPost(serverAddress + ':' + port, { 7 | mode: 'server', 8 | type: 'create', 9 | password: password, 10 | title: 'Server-chan' 11 | }).then(console.log).catch(console.error); 12 | -------------------------------------------------------------------------------- /scripts/createsite.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 4){ 7 | console.error('node createsite.js <SERVER> <MOD>'); 8 | process.exit(); 9 | } 10 | 11 | var server = process.argv[2]; 12 | var mods = process.argv[3]; 13 | 14 | sio.sendPost(serverAddress + ':' + port, { 15 | mode: 'site', 16 | type: 'create', 17 | password: password, 18 | title: 'Smugchan', 19 | formats: ['png', 'jpg'], 20 | mods: [mods], 21 | server: server 22 | }).then(console.log).catch(console.error); 23 | -------------------------------------------------------------------------------- /scripts/createthread.js: -------------------------------------------------------------------------------- 1 | var sio = require('../common/sio.js'); 2 | var serverAddress = require('../common/settings.js').serverAddress; 3 | var password = require('../common/settings.js').password; 4 | var port = 3010; 5 | 6 | if (process.argv.length < 3){ 7 | console.error('node createthread.js <SERVER>'); 8 | process.exit(); 9 | } 10 | 11 | var server = process.argv[2]; 12 | 13 | sio.sendPost(serverAddress + ':' + port, { 14 | mode: 'thread', 15 | type: 'create', 16 | password: password, 17 | title: 'My Thread', 18 | server: server 19 | }).then(console.log).catch(console.error); 20 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smugserver", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "smugdev", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^4.15.4", 14 | "multer": "^1.3.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var multer = require('multer'); 4 | var storage = multer.memoryStorage(); 5 | var upload = multer({ storage: storage }); 6 | var serverAddress = require('../common/settings.js').serverAddress; 7 | var port = 3010; //0 makes the program just pick something free 8 | 9 | var handler = require('../common/inputhandler.js'); 10 | 11 | var globalWrappers = {}; 12 | 13 | //http server entry 14 | app.post('/', upload.single('file'), function (req, res, next) { //TODO work out what next is for 15 | console.log('Post received: ' + JSON.stringify(req.body)); 16 | //console.log(req); 17 | //console.log(req.file); 18 | res.set('Access-Control-Allow-Origin', '*'); 19 | 20 | if (req.body) { 21 | handler.handleInput(globalWrappers, req.body, [req.file], req.connection.remoteAddress, serverAddress).then(obj => { 22 | console.log(obj); 23 | if (obj.status == null) { 24 | res.send(obj.result); 25 | } else { 26 | res.status(obj.status).send(obj.result); 27 | } 28 | }).catch(obj => { 29 | console.error(obj); 30 | if (obj.status == null) { 31 | res.send(obj.result); 32 | } else { 33 | res.status(obj.status).send(obj.result); 34 | } 35 | }); 36 | } else { 37 | res.status(400).send('Malformed POST (missing body).'); 38 | } 39 | }); 40 | 41 | var listener = app.listen(port, function(){ 42 | serverAddress += ':' + listener.address().port; 43 | handler.setupServer(globalWrappers).then(() => { 44 | console.log('Now listening on ' + listener.address().port); 45 | }).catch(console.error); 46 | }); 47 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #set up servers (start_all.sh must be run first) 2 | export IPFS_PATH=./.ipfs 3 | 4 | #create server object 5 | server=$(node scripts/createserver.js) 6 | echo "Server address: $server" 7 | 8 | #create site mod 9 | sitemod=$(node scripts/createmod.js $server) 10 | echo "Site global moderator: $sitemod" 11 | 12 | #create site 13 | site=$(node scripts/createsite.js $server $sitemod) 14 | echo "Site address: $site" 15 | 16 | #get public client 17 | client=$(browserify client/smugchan.js > client/bundle.js && ipfs add -rq client | tail -n1) 18 | echo "Client address: /ipfs/$client" 19 | echo "Complete link: http://localhost:8080/ipfs/$client/#$site" 20 | 21 | #create and add boards 22 | ./populate_boards.sh $server $site 23 | 24 | #create board mod 25 | #boardmod=$(node scripts/createmod.js $server) 26 | 27 | #create board 28 | #board=$(node scripts/createboard.js $server $boardmod $server) 29 | 30 | 31 | 32 | 33 | 34 | #echo "Board address: $board" 35 | #echo "Board moderator: $boardmod" 36 | 37 | -------------------------------------------------------------------------------- /start_all.sh: -------------------------------------------------------------------------------- 1 | IPFS_PATH=./.ipfs ipfs daemon & 2 | echo $! > .ipfs/ipfs.pid 3 | cd nameserver 4 | node namemanager.js >> log.txt & 5 | echo $! > server.pid 6 | sleep 10s #TODO find a better way of working out when ipfs is all set up 7 | #echo "now starting other stuff" 8 | cd ../server 9 | node server.js >> log.txt & 10 | echo $! > server.pid 11 | --------------------------------------------------------------------------------