├── 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 |
52 |
53 |
55 |
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 |
--------------------------------------------------------------------------------