Skip to content
This repository was archived by the owner on Aug 15, 2022. It is now read-only.

Commit 8c35c43

Browse files
committed
feat(chat): PoC
0 parents  commit 8c35c43

12 files changed

+5796
-0
lines changed

.editorconfig

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true
9+
10+
[*.md]
11+
trim_trailing_whitespace = false

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/lib

.eslintrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": [
3+
"oclif",
4+
"oclif-typescript"
5+
],
6+
"root": true,
7+
"rules": {
8+
"node/no-missing-import": 1
9+
}
10+
}

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*-debug.log
2+
*-error.log
3+
/.nyc_output
4+
/dist
5+
/lib
6+
/package-lock.json
7+
/tmp
8+
node_modules

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
peerchat
2+
===============
3+
4+
Peer-to-peer terminal chat based on [DStack](https://github.com/dstack-js/dstack)
5+
6+
[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
7+
[![Version](https://img.shields.io/npm/v/peerchat.svg)](https://npmjs.org/package/peerchat)
8+
[![Downloads/week](https://img.shields.io/npm/dw/peerchat.svg)](https://npmjs.org/package/peerchat)
9+
[![License](https://img.shields.io/npm/l/peerchat.svg)](https://github.com/dstack-js/chat/blob/master/package.json)
10+
11+
<!-- toc -->
12+
* [Usage](#usage)
13+
* [Commands](#commands)
14+
<!-- tocstop -->
15+
# Usage
16+
<!-- usage -->
17+
```sh-session
18+
$ npm install -g peerchat
19+
$ peerchat COMMAND
20+
running command...
21+
$ peerchat (-v|--version|version)
22+
peerchat/0.0.0 darwin-x64 node-v16.13.1
23+
$ peerchat --help [COMMAND]
24+
USAGE
25+
$ peerchat COMMAND
26+
...
27+
```
28+
<!-- usagestop -->
29+
# Commands
30+
<!-- commands -->
31+
32+
<!-- commandsstop -->

bin/run

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
const project = path.join(__dirname, '../tsconfig.json')
6+
const dev = fs.existsSync(project)
7+
8+
if (dev) {
9+
require('ts-node').register({project})
10+
}
11+
12+
require(`../${dev ? 'src' : 'lib'}`).run()
13+
.catch(require('@oclif/errors/handle'))

bin/run.cmd

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@echo off
2+
3+
node "%~dp0\run" %*

package.json

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "peerchat",
3+
"description": "Peer-to-peer terminal chat",
4+
"version": "0.0.0",
5+
"author": "Mykhailo Marynenko @0x77dev",
6+
"bin": {
7+
"peerchat": "./bin/run",
8+
"pchat": "./bin/run"
9+
},
10+
"bugs": "https://github.com/dstack-js/chat/issues",
11+
"dependencies": {
12+
"@dstack-js/ipfs": "^0.2.22",
13+
"@dstack-js/lib": "^0.2.23",
14+
"@oclif/command": "^1",
15+
"@oclif/config": "^1",
16+
"@oclif/plugin-help": "^3",
17+
"blessed": "^0.1.81",
18+
"tslib": "^1",
19+
"wrtc": "^0.4.7"
20+
},
21+
"devDependencies": {
22+
"@oclif/dev-cli": "^1",
23+
"@types/blessed": "^0.1.19",
24+
"@types/node": "^10",
25+
"eslint": "^7.32.0",
26+
"eslint-config-oclif": "^3.1.2",
27+
"eslint-config-oclif-typescript": "^0.2.0",
28+
"ts-node": "^8",
29+
"typescript": "^3.3"
30+
},
31+
"engines": {
32+
"node": ">=12.0.0"
33+
},
34+
"files": [
35+
"/bin",
36+
"/lib"
37+
],
38+
"homepage": "https://github.com/dstack-js/chat",
39+
"keywords": [
40+
"oclif"
41+
],
42+
"license": "GPL-3.0",
43+
"main": "lib/index.js",
44+
"oclif": {
45+
"bin": "peerchat"
46+
},
47+
"repository": "dstack-js/chat",
48+
"scripts": {
49+
"posttest": "eslint . --ext .ts --config .eslintrc",
50+
"prepack": "rm -rf lib && tsc -b && oclif-dev readme",
51+
"test": "echo NO TESTS",
52+
"version": "oclif-dev readme && git add README.md"
53+
},
54+
"types": "lib/index.d.ts"
55+
}

src/chat.ts

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/* eslint-disable node/no-extraneous-import */
2+
/* eslint-disable no-process-exit */
3+
/* eslint-disable unicorn/no-process-exit */
4+
import {create} from '@dstack-js/ipfs'
5+
import {Stack} from '@dstack-js/lib'
6+
import {PubSub} from '@dstack-js/lib/src/pubsub'
7+
const wrtc = require('wrtc')
8+
import * as blessed from 'blessed'
9+
10+
interface Message {
11+
nickname?: string;
12+
message: string;
13+
}
14+
15+
export const run = async (room: string, nickname?: string) => {
16+
const ipfs = await create({
17+
repo: process.env.IPFS_REPO,
18+
relay: {
19+
enabled: true, // enable relay dialer/listener (STOP)
20+
hop: {
21+
enabled: true, // make this node a relay (HOP)
22+
},
23+
},
24+
config: {
25+
Addresses: {
26+
Swarm: ['/ip4/0.0.0.0/tcp/0', '/dns4/dstack-relay.herokuapp.com/tcp/443/wss/p2p-webrtc-star'],
27+
},
28+
Discovery: {
29+
MDNS: {
30+
Enabled: true,
31+
Interval: 1,
32+
},
33+
webRTCStar: {
34+
Enabled: true,
35+
},
36+
},
37+
Bootstrap: ['/dns4/dstack-relay.herokuapp.com/tcp/443/wss/p2p-webrtc-star/p2p/QmV2uXBKbii29iJKHKVy8sx5m49qdDTBYNybVoa5uLJtrf'],
38+
},
39+
}, wrtc)
40+
41+
const stack = await Stack.create('dstack-chat', ipfs)
42+
const pubsub = stack.pubsub as PubSub<Message>
43+
44+
const screen = blessed.screen({
45+
smartCSR: true,
46+
title: `#${room}`,
47+
})
48+
49+
const messageList = blessed.list({
50+
align: 'left',
51+
mouse: true,
52+
keys: true,
53+
width: '100%',
54+
height: '90%',
55+
border: 'line',
56+
top: 0,
57+
left: 0,
58+
items: [
59+
'dstack: connected',
60+
`dstack: your id is ${stack.id.slice(-5)}`,
61+
'dstack: use /help to see commands',
62+
],
63+
})
64+
65+
// Append our box to the screen.
66+
const input = blessed.textarea({
67+
bottom: 0,
68+
height: '10%',
69+
inputOnFocus: true,
70+
clickable: true,
71+
label: ` ID: ${stack.id.slice(-5)} `,
72+
padding: {
73+
top: 1,
74+
left: 2,
75+
},
76+
style: {
77+
fg: '#787878',
78+
bg: '#454545',
79+
80+
focus: {
81+
fg: '#f6f6f6',
82+
bg: '#353535',
83+
},
84+
},
85+
})
86+
87+
input.key('enter', async function () {
88+
const message = input.getValue()
89+
if (message.length === 0) return
90+
91+
if (message.startsWith('/connect ')) {
92+
await stack.ipfs.swarm.connect(message.split('/connect ')[1])
93+
input.clearValue()
94+
screen.render()
95+
return
96+
}
97+
98+
if (message.startsWith('/addr')) {
99+
const {addresses} = await ipfs.id()
100+
101+
for (const addr of addresses) {
102+
messageList.addItem(`dstack: ${addr.toString()}`)
103+
}
104+
105+
input.clearValue()
106+
screen.render()
107+
return
108+
}
109+
110+
if (message.startsWith('/help')) {
111+
messageList.addItem('dstack: /addr - your addresses')
112+
messageList.addItem('dstack: /connect <addr> - manually connect to peer')
113+
messageList.addItem('dstack: /peers - peers list')
114+
input.clearValue()
115+
screen.render()
116+
return
117+
}
118+
119+
if (message.startsWith('/peers')) {
120+
const peers = await stack.peers()
121+
122+
for (const peer of peers) {
123+
messageList.addItem(`dstack: ${peer.id}`)
124+
}
125+
126+
input.clearValue()
127+
screen.render()
128+
return
129+
}
130+
131+
try {
132+
await pubsub.publish('chat', {nickname, message})
133+
} catch {
134+
// error handling
135+
} finally {
136+
input.clearValue()
137+
screen.render()
138+
}
139+
})
140+
141+
screen.key(['escape', 'q', 'C-c'], function () {
142+
return process.exit(0)
143+
})
144+
145+
screen.append(messageList)
146+
screen.append(input)
147+
input.focus()
148+
149+
await pubsub.subscribe('chat', event => {
150+
messageList.addItem(`${event.data.nickname ? `${event.data.nickname} (${event.from.slice(-5)})` : event.from.slice(-5)}: ${event.data.message}`)
151+
messageList.scrollTo(100)
152+
screen.render()
153+
})
154+
155+
stack.onPeerConnect(async peer => {
156+
messageList.setLabel(` Messages #${room} - Peers: ${await pubsub.peers('chat')} `)
157+
messageList.addItem(`dstack: peer connected ${peer.id.slice(-5)}`)
158+
messageList.scrollTo(100)
159+
screen.render()
160+
})
161+
162+
setInterval(async () => {
163+
messageList.setLabel(` Messages #${room} - Peers: ${await pubsub.peers('chat')} `)
164+
screen.render()
165+
}, 1000)
166+
167+
messageList.scrollTo(100)
168+
screen.render()
169+
}

src/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Command, flags} from '@oclif/command'
2+
import {run} from './chat'
3+
4+
class DstackJsChat extends Command {
5+
static description = 'start chat'
6+
7+
static flags = {
8+
version: flags.version({char: 'v'}),
9+
}
10+
11+
static examples = [
12+
'$ peerchat',
13+
'$ peerchat ROOM NICKNAME',
14+
'$ peerchat dstack myCoolNickname',
15+
]
16+
17+
static args = [{name: 'room', default: 'dstack', description: 'chat room'}, {name: 'nickname', description: 'your nickname'}]
18+
19+
async run() {
20+
const {args} = this.parse(DstackJsChat)
21+
run(args.room, args.nickname)
22+
}
23+
}
24+
25+
export = DstackJsChat

tsconfig.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"declaration": true,
4+
"importHelpers": true,
5+
"module": "commonjs",
6+
"outDir": "lib",
7+
"rootDir": "src",
8+
"strict": true,
9+
"target": "es2017",
10+
"typeRoots": ["./node_modules/@types"]
11+
},
12+
"include": [
13+
"src/**/*"
14+
]
15+
}

0 commit comments

Comments
 (0)