├── .eslintrc ├── .gitignore ├── bin └── index.js ├── hyperchat.js ├── listener.js ├── package.json └── readme.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | chats/ 3 | remote-chats/ 4 | *.log 5 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const inquirer = require('inquirer') 3 | const Hyperchat = require('../hyperchat') 4 | 5 | const setupQuestions = [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'What is your name?' 9 | }] 10 | 11 | const openInput = { 12 | type: 'input', 13 | name: 'input', 14 | message: '>>>' 15 | } 16 | 17 | let chat 18 | 19 | inquirer.prompt(setupQuestions) 20 | .then((answers) => { 21 | // start chatclient 22 | if (answers.name) { 23 | chat = new Hyperchat(answers.name) 24 | chat.on('ready', () => console.log(chat.name, 'now available on public key:', chat.key)) 25 | chat.on('connection', () => console.log('i am connected to someone')) 26 | chat.on('listening', data => console.log('i am listening to', data.key)) 27 | chat.on('disconnecting', key => console.log('disconnecting from', key)) 28 | chat.on('disconnected', key => console.log('disconnected from', key)) 29 | chat.on('destroyed', () => console.log('Hyperchat is destroyed')) 30 | chat.on('started', data => console.log(data.name, 'joined conversation')) 31 | chat.on('ended', data => console.log(data.name, 'exited conversation')) 32 | chat.on('heard', data => console.log(data.name, 'heard', data.who, '-', data.index)) 33 | chat.on('message', data => console.log(`${data.name}:`, data.message)) 34 | return chatLoop() 35 | } 36 | }) 37 | 38 | function chatLoop () { 39 | return inquirer.prompt([openInput]) 40 | .then(actOnInput) 41 | } 42 | 43 | function actOnInput (answer) { 44 | const { input } = answer 45 | const command = input.match(/:(\w+)(?:\s(.+))?/) 46 | if (command) { 47 | switch (command[1]) { 48 | case 'q': 49 | case 'quit': 50 | chat.disconnect(() => console.log('DESTROYED')) 51 | console.log('QUIT') 52 | return 53 | case 'c': 54 | case 'connect': 55 | if (command[2]) { 56 | const key = command[2].trim() 57 | console.log('attempt to connect to', key) 58 | chat.add(key) 59 | } 60 | break 61 | case 'w': 62 | case 'whoami': 63 | console.log('your public key is:', chat.key) 64 | break 65 | case 'd': 66 | case 'disconnect': 67 | const key = command[2].trim() 68 | console.log('attempt to disconnect from', key) 69 | chat.remove(key) 70 | break 71 | case 'h': 72 | case 'help': 73 | console.log(':h or :help - prints out the commants the chat cli accepts') 74 | console.log(':w or :whoami - logs out your address') 75 | console.log(':c [key] or :connect [key] - listens to conversation at key') 76 | console.log(':d [key] or :disconnect [key] - stops listnening to conversation at key') 77 | console.log(':q or :quit [key] - stops sharing and kills all connections') 78 | break 79 | } 80 | } else { 81 | // append chat 82 | chat.chat(input) 83 | } 84 | chatLoop() 85 | } 86 | -------------------------------------------------------------------------------- /hyperchat.js: -------------------------------------------------------------------------------- 1 | const hypercore = require('hypercore') 2 | const hyperdiscovery = require('hyperdiscovery') 3 | const Listener = require('./listener') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const events = require('events') 7 | const homedir = require('os').homedir() 8 | 9 | const chatsDirectory = path.resolve(homedir, './hyperchats') 10 | 11 | class Hyperchat extends events.EventEmitter { 12 | constructor (name) { 13 | super() 14 | this.name = name 15 | try { 16 | fs.statSync(chatsDirectory) 17 | } catch (e) { 18 | fs.mkdirSync(chatsDirectory) 19 | } 20 | this.feed = hypercore(path.join(chatsDirectory, name), { valueEncoding: 'json' }) 21 | this.listeningTo = [] 22 | this.ready = false 23 | this.feed.on('ready', () => this.connect()) 24 | this.feed.on('error', e => console.error('feed error:', e)) 25 | this.swarm = undefined 26 | } 27 | 28 | get key () { 29 | return this.feed.key.toString('hex') 30 | } 31 | 32 | get discoveryKey () { 33 | return this.feed.discoveryKey.toString('hex') 34 | } 35 | 36 | connect () { 37 | this.feed.append({ time: Date.now(), name: this.name, status: 'START' }, (err) => { 38 | if (err) throw err 39 | this.emit('ready') 40 | this.ready = true 41 | const archive = this.feed 42 | this.swarm = hyperdiscovery(this.feed, { 43 | stream: function (peer) { 44 | const stream = archive.replicate({ 45 | live: true, 46 | upload: true, 47 | download: true, 48 | userData: archive.key 49 | }) 50 | stream.on('handshake', () => { 51 | console.log('HANDSHAKE RECIEVER', stream.remoteUserData.toString('hex')) 52 | }) 53 | return stream 54 | } 55 | }) 56 | this.swarm.once('connection', () => { this.emit('connection') }) 57 | }) 58 | } 59 | 60 | disconnect (cb) { 61 | const kill = () => { 62 | if (count === 0 && this.swarm) { 63 | this.feed.append({ time: Date.now(), name: this.name, status: 'END' }, () => { 64 | setTimeout(() => this._destroy(cb), 40) 65 | }) 66 | }; 67 | } 68 | var count = this.listeningTo.length 69 | if (count) { 70 | this.listeningTo.forEach((remote) => { 71 | this.emit('disconnecting', remote.key) 72 | remote.disconnect(() => { 73 | this.emit('disconnected', remote.key) 74 | count-- 75 | kill() 76 | }) 77 | }) 78 | } else { 79 | kill() 80 | } 81 | } 82 | 83 | chat (msg) { 84 | if (this.ready) { 85 | this.feed.append({ time: Date.now(), msg }) 86 | } else { 87 | console.warn('Feed is not ready yet') 88 | } 89 | } 90 | 91 | heard (discoveryKey, index) { 92 | this.feed.append({ time: Date.now(), heard: discoveryKey, index, status: 'HEARD' }) 93 | } 94 | 95 | add (key) { 96 | const remote = new Listener(key, this) 97 | // attach listener events 98 | this.listeningTo.push(remote) 99 | } 100 | 101 | remove (key) { 102 | const id = this.listeningTo.findIndex(e => e.key === key) 103 | if (id >= 0) { 104 | this.listeningTo[id].disconnect() 105 | this.listeningTo.splice(id, 1) 106 | } else { 107 | console.warn('You dont seem to be connected to:', key) 108 | } 109 | } 110 | 111 | _destroy (cb) { 112 | this.swarm.leave(this.feed.discoveryKey) 113 | this.swarm.destroy(cb) 114 | this.swarm = undefined 115 | this.emit('destroyed') 116 | } 117 | } 118 | 119 | module.exports = Hyperchat 120 | -------------------------------------------------------------------------------- /listener.js: -------------------------------------------------------------------------------- 1 | const hypercore = require('hypercore') 2 | const hyperdiscovery = require('hyperdiscovery') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const homedir = require('os').homedir() 6 | 7 | const remoteChatDirectory = path.resolve(homedir, './hyperchats/remote') 8 | 9 | class Listener { 10 | constructor (key, receiver) { 11 | this.swarm = undefined 12 | this.name = undefined 13 | try { 14 | fs.statSync(remoteChatDirectory) 15 | } catch (e) { 16 | fs.mkdirSync(remoteChatDirectory) 17 | } 18 | this.receiver = receiver 19 | this.feed = hypercore(path.join(remoteChatDirectory, key), key, { valueEncoding: 'json', sparse: true, live: true }) 20 | this.feed.on('ready', () => this.connect()) 21 | this.feed.on('error', e => console.error('feed error:', e)) 22 | this.lastVersion = undefined 23 | // start listening to append messages 24 | this.feed.on('append', this._newData.bind(this)) 25 | } 26 | 27 | get key () { 28 | return this.feed.key.toString('hex') 29 | } 30 | 31 | connect () { 32 | console.log('Listening to:', this.key) 33 | const archive = this.feed 34 | const receiver = this.receiver.feed 35 | this.swarm = hyperdiscovery(this.feed, { 36 | stream: function (peer) { 37 | const stream = archive.replicate({ 38 | live: true, 39 | upload: true, 40 | download: true, 41 | userData: receiver.key 42 | }) 43 | stream.on('handshake', () => { 44 | console.log('HANDSHAKE LISTENER', stream.remoteUserData.toString('hex')) 45 | }) 46 | return stream 47 | } 48 | }) 49 | this.swarm.once('connection', () => { 50 | this.lastVersion = this.feed.length 51 | this.receiver.emit('listening', { key: this.key }) 52 | this.feed.get(0, this._setName.bind(this)) 53 | this._update() 54 | }) 55 | } 56 | 57 | _setName (err, data) { 58 | if (err) throw err 59 | this.name = data.name || this.key 60 | } 61 | 62 | _update () { 63 | this.feed.update(() => { 64 | this.lastVersion = this.feed.length 65 | this._update() 66 | }) 67 | } 68 | 69 | _newData () { 70 | const last = this.lastVersion || 0 71 | const newest = this.feed.length 72 | if (newest > last) { 73 | this.lastVersion = newest 74 | this.feed.download({start: last, end: newest}, () => { 75 | for (var i = last; i < newest; i++) { 76 | const index = i 77 | this.feed.get( 78 | index, 79 | {wait: false, valueEncoding: 'json'}, 80 | (err, data) => this._gotMessage(err, data, index) 81 | ) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | _gotMessage (err, data, index) { 88 | if (err) throw err 89 | if (data.msg) { 90 | this.receiver.emit('message', { 91 | name: this.name, 92 | message: data.msg, 93 | time: data.time, 94 | index 95 | }) 96 | this.receiver.heard(this.discoveryKey, index) 97 | } 98 | switch (data.status) { 99 | case 'START': 100 | this.receiver.emit('started', { name: this.name }) 101 | break 102 | case 'END': 103 | this.receiver.emit('ended', { name: this.name }) 104 | break 105 | case 'HEARD': 106 | const who = data.heard === this.discoveryKey ? 'you' : data.heard 107 | this.receiver.emit('heard', {name: this.name, who, index: data.index}) 108 | 109 | break 110 | } 111 | } 112 | 113 | disconnect (cb) { 114 | if (this.swarm) { 115 | this._destroy(cb) 116 | } 117 | } 118 | 119 | _destroy (cb) { 120 | this.swarm.leave(this.feed.discoveryKey) 121 | this.swarm.destroy(cb) 122 | this.swarm = undefined 123 | } 124 | } 125 | 126 | module.exports = Listener 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e-e-e/hyperchat", 3 | "version": "0.0.4", 4 | "description": "hypercore + chat", 5 | "main": "hyperchat.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Benjamin Forster", 10 | "license": "ISC", 11 | "bin": { 12 | "hyperchat": "./bin/index.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/e-e-e/hyperchat" 17 | }, 18 | "dependencies": { 19 | "discovery-swarm": "^4.4.0", 20 | "hypercore": "^6.6.3", 21 | "hyperdiscovery": "^6.0.4", 22 | "inquirer": "^3.1.1" 23 | }, 24 | "devDependencies": { 25 | "eslint-config-standard": "^10.2.1", 26 | "standard": "^10.0.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Hyperchat 2 | 3 | An experiment with using hypercore to share and record chat histories. 4 | 5 | ## install 6 | 7 | There are two options. 8 | 9 | ### 1. Globally 10 | 11 | Install globally to use from the command line as a chat client: 12 | ```bash 13 | npm install -g @e-e-e/hyperchat 14 | hyperchat 15 | # you will be promped to add a username 16 | ? What is your name? ... 17 | # this will create a hypercore file in your home directory for this name 18 | # from the chat interface you can enter any of the following commands 19 | ? >>> 20 | # :h or :help - prints out the comments the chat cli accepts 21 | # :w or :whoami - logs out your address 22 | # :c [key] or :connect [key] - listens to conversation at key 23 | # :d [key] or :disconnect [key] - stops listnening to conversation at key 24 | # :q or :quit [key] - stops sharing and kills all connections 25 | ``` 26 | 27 | ### 2. Package 28 | Install and build your own chat app on top of hyperchat 29 | ```bash 30 | npm install @e-e-e/hyperchat --save 31 | ``` 32 | and import in your project; 33 | ```js 34 | var hyperchat = require('@e-e-e/hyperchat') 35 | var chat = new Hyperchat('username') 36 | 37 | // you can listen to chat events 38 | chat.on('ready', () => console.log(chat.name, 'now available on public key:', chat.key)) 39 | chat.on('connection', () => console.log('i am connected to someone')) 40 | chat.on('listening', data => console.log('i am listening to', data.key)) 41 | chat.on('disconnecting', key => console.log('disconnecting from', key)) 42 | chat.on('disconnected', key => console.log('disconnected from', key)) 43 | chat.on('destroyed', () => console.log('Hyperchat is destroyed')) 44 | chat.on('started', data => console.log(data.name, 'joined conversation')) 45 | chat.on('ended', data => console.log(data.name, 'exited conversation')) 46 | chat.on('heard', data => console.log(data.name, 'heard', data.who, '-', data.index)) 47 | chat.on('message', data => console.log(`${data.name}:`, data.message)) 48 | 49 | // connect to multiple other clients 50 | chat.add('some-public-key') 51 | chat.add('some-other-public-key') 52 | 53 | // disconnect from other clients 54 | chat.remove('some-public-key') 55 | 56 | // and chat to any client who is also connected to you 57 | chat.chat('hello world') 58 | ``` 59 | 60 | ## config 61 | 62 | At the moment there are no config options exposed. 63 | Chats are by default stored at the users home: `~/hyperchats` 64 | --------------------------------------------------------------------------------