├── README.md ├── package.json ├── LICENSE ├── .gitignore ├── index.html └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # cabal-web 2 | A bare-bones Cabal chat implementation for the web. 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cabal-web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A bare-bones Cabal chat implementation for the web.", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "browserify ./ > bundle.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/RangerMauve/cabal-web.git" 14 | }, 15 | "author": "rangermauve", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/RangerMauve/cabal-web/issues" 19 | }, 20 | "homepage": "https://github.com/RangerMauve/cabal-web#readme", 21 | "dependencies": { 22 | "cabal-core": "^5.0.1", 23 | "discovery-swarm-web": "^1.0.7", 24 | "random-access-web": "^2.0.1" 25 | }, 26 | "devDependencies": { 27 | "browserify": "^16.2.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | -------------------------------------------------------------------------------- /.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 (https://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 | # next.js build output 61 | .next 62 | 63 | package-lock.json 64 | bundle.js 65 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | Cabal Web 3 | 48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const url = new URL(window.location.href) 2 | 3 | let key = url.searchParams.get('key') 4 | 5 | if(!key) { 6 | const gotKey = prompt("Enter a cabal key to join", 'cabal://14bc77d788fdaf07b89b28e9d276e47f2e44011f4adb981921056e1b3b40e99e') 7 | 8 | window.location.search = `?key=${gotKey}` 9 | } 10 | 11 | if(key.startsWith('cabal://')) { 12 | key = key.slice('cabal://'.length) 13 | } 14 | 15 | key = Buffer.from(key, 'hex') 16 | 17 | const Cabal = require('cabal-core') 18 | const crypto = require('crypto') 19 | const RAW = require('random-access-web') 20 | const DiscoverySwarmWeb = require('discovery-swarm-web') 21 | 22 | // This discovery server supports the default handshaking from discovery-swarm which is needed for cabal 23 | const DISCOVERY_SERVER = 'wss://rawswarm.mauve.moe' 24 | 25 | const storage = RAW('cabal') 26 | const cabalStorage = (file) => storage(key + '/' + file) 27 | 28 | const cabal = new Cabal(cabalStorage, key) 29 | 30 | window.loadChannel = loadChannel 31 | 32 | cabal.channels.events.on('add', renderChannels) 33 | 34 | cabal.getLocalKey((err, key) => { 35 | if(err) throw err 36 | 37 | const joinKey = sha1(cabal.key.toString('hex')).slice(0, 20) 38 | 39 | console.log('joinKey', joinKey.toString('hex')) 40 | 41 | const swarm = new DiscoverySwarmWeb({ 42 | stream, 43 | discovery: DISCOVERY_SERVER, 44 | id: Buffer.from(key) 45 | }) 46 | 47 | swarm.join(joinKey) 48 | }) 49 | 50 | let currentChannel = null 51 | 52 | loadChannel('default') 53 | 54 | $('#controls').addEventListener('submit', (e) => { 55 | e.preventDefault() 56 | const messageInput = $('#message') 57 | 58 | const message = messageInput.value 59 | if(!message) return 60 | 61 | messageInput.value = '' 62 | 63 | writeMessage(message) 64 | }) 65 | 66 | function renderChannels() { 67 | cabal.channels.get((err, channels) => { 68 | const contents = channels.map((channel) => ` 69 | 70 | `).join('\n') 71 | 72 | $('#channels').innerHTML = contents 73 | }) 74 | } 75 | 76 | function stream(info) { 77 | console.log('Replicating', info) 78 | return cabal.replicate() 79 | } 80 | 81 | function writeMessage(text) { 82 | if(text.startsWith('/nick')) { 83 | const nick = text.slice('/nick '.length) 84 | cabal.publishNick(nick) 85 | return 86 | } 87 | 88 | cabal.publish({ 89 | type: 'chat/text', 90 | content: { 91 | text, 92 | channel: currentChannel 93 | } 94 | }) 95 | } 96 | 97 | function addMessage({key, seq, value}, prepend) { 98 | const {content, type, timestamp} = value 99 | const {channel, text} = content 100 | 101 | // Don't show messages from other channels 102 | if(channel !== currentChannel) return 103 | 104 | cabal.getUser(key, (err, user) => { 105 | let name = `Anon-${key.slice(0,8)}` 106 | if(user) name = user.name 107 | console.log(timestamp, name, text) 108 | const contents = ` 109 | ${prettyTimestamp(timestamp)} 110 | ${name}: 111 | ${text} 112 | ` 113 | 114 | const item = document.createElement('div') 115 | item.classList.add('message') 116 | 117 | item.innerHTML = contents 118 | if(prepend) { 119 | $('#messages').insertBefore(item, $('#messages').firstChild) 120 | } else { 121 | $('#messages').appendChild(item) 122 | } 123 | }) 124 | } 125 | 126 | function loadChannel(channel) { 127 | if(currentChannel) { 128 | cabal.messages.events.removeListener(currentChannel, addMessage) 129 | } 130 | 131 | currentChannel = channel 132 | 133 | $('#messages').innerHTML = `
Loading channel ${channel}
` 134 | 135 | // Read messages 136 | cabal.messages.read(channel, { 137 | limit: 16, 138 | }) 139 | // Render the latest 16 140 | .on('data', (message) => { 141 | addMessage(message, true) 142 | }) 143 | // Start listening for new messages 144 | .on('end', () => { 145 | cabal.messages.events.on(channel, addMessage) 146 | }) 147 | } 148 | 149 | function prettyTimestamp(timestamp) { 150 | const date = new Date(timestamp) 151 | const year = date.getFullYear() 152 | const month = zeropad(date.getMonth()+1) 153 | const day = zeropad(date.getDate()) 154 | const hours = zeropad(date.getHours()) 155 | const minutes= zeropad(date.getMinutes()) 156 | return `${year}/${month}/${day} ${hours}:${minutes}` 157 | } 158 | 159 | function zeropad(number) { 160 | if(number < 10) { 161 | return `0${number}` 162 | } 163 | return number 164 | } 165 | 166 | function $(selector) { 167 | return document.querySelector(selector) 168 | } 169 | 170 | function sha1 (id) { 171 | return crypto.createHash('sha1').update(id).digest() 172 | } 173 | --------------------------------------------------------------------------------