-
Notifications
You must be signed in to change notification settings - Fork 73
Expand file tree
/
Copy pathdirectchat.nim
More file actions
192 lines (159 loc) · 5.08 KB
/
Copy pathdirectchat.nim
File metadata and controls
192 lines (159 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
{.used.}
when not (compileOption("threads")):
{.fatal: "Please, compile this program with the --threads:on option!".}
import strformat, strutils, stew/byteutils, chronos, libp2p
const DefaultAddr = "/ip4/127.0.0.1/tcp/0"
const Help = """
Commands: /[?|help|connect|disconnect|exit]
help: Prints this help
connect: dials a remote peer
disconnect: ends current session
exit: closes the chat
"""
type Chat = ref object
switch: Switch # a single entry point for dialing and listening to peer
stdinReader: StreamTransport # transport streams between read & write file descriptor
stream: Stream # stream to the other peer
connected: bool # if the node is connected to another peer
##
# Stdout helpers, to write the prompt
##
proc writePrompt(c: Chat) =
if c.connected:
stdout.write '\r' & $c.switch.peerInfo.peerId & ": "
stdout.flushFile()
proc writeStdout(c: Chat, str: string) =
echo '\r' & str
c.writePrompt()
##
# Chat Protocol
##
const ChatCodec = "/nim-libp2p/chat/1.0.0"
type ChatProto = ref object of LPProtocol
proc new(T: typedesc[ChatProto], c: Chat): T =
let chatproto = T()
# create handler for incoming stream
proc handle(stream: Stream, proto: string) {.async: (raises: [CancelledError]).} =
try:
if c.connected and not c.stream.closed:
c.writeStdout "a chat session is already in progress - refusing incoming peer!"
else:
await c.handlePeer(stream)
except IOError as exc:
echo "exception in handler", exc.msg
finally:
await stream.close()
# assign the new handler
chatproto.handler = handle
chatproto.codec = ChatCodec
return chatproto
##
# Chat application
##
proc handlePeer(
c: Chat, stream: Stream
) {.async: (raises: [CancelledError, IOError]).} =
# Handle a peer (incoming or outgoing)
try:
c.stream = stream
c.connected = true
c.writeStdout $stream.peerId & " connected"
# Read loop
while true:
let
strData = await stream.readLp(1024)
str = string.fromBytes(strData)
c.writeStdout $stream.peerId & ": " & $str
except LPStreamError:
defer:
c.writeStdout $stream.peerId & " disconnected"
await c.stream.close()
c.connected = false
proc dialPeer(c: Chat, address: string) {.async.} =
# Parse and dial address
let
multiAddr = MultiAddress.init(address).tryGet()
# split the peerId part /p2p/...
peerIdBytes = multiAddr[multiCodec("p2p")].tryGet().protoAddress().tryGet()
remotePeer = PeerId.init(peerIdBytes).tryGet()
# split the wire address
ip4Addr = multiAddr[multiCodec("ip4")].tryGet()
tcpAddr = multiAddr[multiCodec("tcp")].tryGet()
wireAddr = ip4Addr & tcpAddr
echo &"dialing peer: {multiAddr}"
asyncSpawn c.handlePeer(await c.switch.dial(remotePeer, @[wireAddr], ChatCodec))
proc readLoop(c: Chat) {.async.} =
while true:
if not c.connected:
echo "type an address or wait for a connection:"
echo "type /[help|?] for help"
c.writePrompt()
let line = await c.stdinReader.readLine()
if line.startsWith("/help") or line.startsWith("/?"):
echo Help
continue
if line.startsWith("/disconnect"):
c.writeStdout "Ending current session"
if c.connected and c.stream.closed.not:
await c.stream.close()
c.connected = false
elif line.startsWith("/connect"):
c.writeStdout "enter address of remote peer"
let address = await c.stdinReader.readLine()
if address.len > 0:
await c.dialPeer(address)
elif line.startsWith("/exit"):
if c.connected and c.stream.closed.not:
await c.stream.close()
c.connected = false
await c.switch.stop()
c.writeStdout "quitting..."
return
else:
if c.connected:
await c.stream.writeLp(line)
else:
if line.startsWith("/") and "p2p" in line:
await c.dialPeer(line)
proc readInput(wfd: AsyncFD) {.thread.} =
## This thread performs reading from `stdin` and sends data over
## pipe to main thread.
let transp = fromPipe(wfd)
while true:
let line = stdin.readLine()
discard waitFor transp.write(line & "\r\n")
proc main() {.async.} =
let
rng = newRng() # Single random number source for the whole application
# Pipe to read stdin from main thread
(rfd, wfd) = createAsyncPipe()
stdinReader = fromPipe(rfd)
var thread: Thread[AsyncFD]
try:
thread.createThread(readInput, wfd)
except Exception as exc:
quit("Failed to create thread: " & exc.msg)
var localAddress = MultiAddress.init(DefaultAddr).tryGet()
var switch = SwitchBuilder
.new()
.withRng(rng)
# Give the application RNG
.withAddress(localAddress)
.withTcpTransport()
# Use TCP as transport
.withMplex()
# Use Mplex as muxer
.withNoise()
# Use Noise as secure manager
.build()
let chat = Chat(switch: switch, stdinReader: stdinReader)
switch.mount(ChatProto.new(chat))
await switch.start()
let id = $switch.peerInfo.peerId
echo "PeerId: " & id
echo "listening on: "
for a in switch.peerInfo.addrs:
echo &"{a}/p2p/{id}"
await chat.readLoop()
quit(0)
waitFor(main())