Skip to content

Commit f90d46d

Browse files
committed
Initial commit
0 parents  commit f90d46d

File tree

5 files changed

+243
-0
lines changed

5 files changed

+243
-0
lines changed

nimchat.nimble

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Package
2+
3+
version = "0.1.0"
4+
author = "zedeus"
5+
description = "A chat application"
6+
license = "GPL-3.0"
7+
srcDir = "src"
8+
9+
10+
# Dependencies
11+
12+
requires "nim >= 0.19.0"

src/client.nim

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import asyncdispatch, asyncnet, os, threadpool, strutils, strformat
2+
import ./protocol
3+
4+
type
5+
Client = ref object
6+
socket: AsyncSocket
7+
server: string
8+
port: int
9+
username: string
10+
11+
proc displayMessage(message: Message) =
12+
echo &"{message.username}: {message.text}"
13+
14+
proc connect(client: Client) {.async.} =
15+
echo "Connecting to ", client.server
16+
await connect(client.socket, client.server, Port(client.port))
17+
echo "Connected!"
18+
19+
proc login(client: Client) {.async.} =
20+
echo "Logging in to ", client.server
21+
let message = createMessage(client.username, "login", ConnectionMessage)
22+
await client.socket.send(message)
23+
24+
let response = await client.socket.recvLine()
25+
let parsed = parseMessage(response)
26+
27+
if parsed.mtype == ServiceMessage:
28+
displayMessage(parsed)
29+
elif parsed.mtype == ErrorMessage:
30+
quit("ERROR: " & parsed.text)
31+
32+
proc processMessages(client: Client) {.async.} =
33+
while true:
34+
let line = await client.socket.recvLine()
35+
let parsed = parseMessage(line)
36+
displayMessage(parsed)
37+
38+
proc processInput(client: Client) {.async.} =
39+
var messageFlowVar = spawn stdin.readLine()
40+
while true:
41+
if messageFlowVar.isReady():
42+
let message = createMessage(client.username, ^messageFlowVar, TextMessage)
43+
asyncCheck client.socket.send(message)
44+
messageFlowVar = spawn stdin.readLine()
45+
poll()
46+
47+
proc newClient(): Client =
48+
if paramCount() < 3:
49+
quit("Please specify server address, port and username\nExample: ./client localhost 7687 user")
50+
51+
new(result)
52+
result.server = paramStr(1)
53+
result.port = parseInt(paramStr(2))
54+
result.username = paramStr(3)
55+
result.socket = newAsyncSocket()
56+
57+
proc main() =
58+
let client = newClient()
59+
waitFor client.connect()
60+
waitFor client.login()
61+
62+
asyncCheck client.processMessages()
63+
waitFor client.processInput()
64+
65+
main()

src/client.nims

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--threads:on

src/protocol.nim

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import json, hashes, strutils
2+
3+
type
4+
MessageType* = enum
5+
TextMessage, ServiceMessage, ConnectionMessage,
6+
WarningMessage, ErrorMessage
7+
8+
Message* = object
9+
username*: string
10+
mtype*: MessageType
11+
text*: string
12+
hash*: int
13+
14+
proc getHash*(username, text: string): int =
15+
return hash(username & text)
16+
17+
proc getHash*(message: Message): int =
18+
return hash(message.username & message.text)
19+
20+
proc verifyMessage*(message: Message): bool =
21+
return getHash(message) == message.hash
22+
23+
proc parseMessage*(data: string): Message =
24+
let dataJson = parseJson(data)
25+
result.username = dataJson["username"].getStr()
26+
result.text = dataJson["text"].getStr()
27+
result.mtype = parseEnum[MessageType](dataJson["mtype"].getStr())
28+
result.hash = dataJson["hash"].getInt()
29+
30+
proc createMessage*(username, text: string; mtype: MessageType): string =
31+
result = $(%{
32+
"username": %username,
33+
"text": %text,
34+
"mtype": %mtype,
35+
"hash": %getHash(username, text)
36+
}) & "\c\l"
37+
38+
39+
when isMainModule:
40+
let hash = getHash("John" & "Hi!")
41+
let data = """{"username": "John", "text": "Hi!", "hash": 6635146331310275049, "mtype": 0}"""
42+
43+
let parsed = parseMessage(data)
44+
doAssert parsed.username == "John"
45+
doAssert parsed.text == "Hi!"
46+
doAssert parsed.hash == getHash("John" & "Hi!")
47+
echo "All tests passed"

src/server.nim

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import asyncdispatch, asyncnet, os, strformat, strutils
2+
import ./protocol
3+
4+
type
5+
Client = ref object
6+
socket: AsyncSocket
7+
netAddr: string
8+
connected: bool
9+
username: string
10+
id: int
11+
12+
Server = ref object
13+
socket: AsyncSocket
14+
clients: seq[Client]
15+
16+
proc newServer(): Server =
17+
Server(socket: newAsyncSocket(), clients: @[])
18+
19+
proc `$`(client: Client): string =
20+
&"{client.id} ({client.netAddr})"
21+
22+
proc sendMessage(client: Client; text: string; mtype: MessageType) {.async.} =
23+
let message = createMessage("server", text, mtype)
24+
await client.socket.send(message)
25+
26+
proc sendError(client: Client; text: string) {.async.} =
27+
await sendMessage(client, text, ErrorMessage)
28+
29+
proc sendWarning(client: Client; text: string) {.async.} =
30+
await sendMessage(client, text, WarningMessage)
31+
32+
proc sendService(client: Client; text: string) {.async.} =
33+
if not client.connected: return
34+
await sendMessage(client, text, ServiceMessage)
35+
36+
proc relayMessage(server: Server; message: string; skip=(-1)) {.async.} =
37+
for c in server.clients:
38+
if c.connected and (skip == -1 or c.id != skip):
39+
await c.socket.send(message)
40+
41+
proc broadcast(server: Server; text: string; skip=(-1); mtype=ServiceMessage) {.async.} =
42+
let message = createMessage("server", text, mtype)
43+
await relayMessage(server, message, skip)
44+
45+
proc disconnect(client: Client) =
46+
echo client, " disconnected!"
47+
client.connected = false
48+
client.username = ""
49+
client.socket.close()
50+
51+
proc handleLogin(server: Server; client: Client; message: Message) {.async.} =
52+
if client.connected:
53+
return
54+
55+
for c in server.clients:
56+
if c.username == message.username:
57+
await sendError(client, "Username already taken.")
58+
return
59+
60+
client.username = message.username
61+
client.connected = true
62+
63+
await sendService(client, "Login succesful.")
64+
await server.broadcast(&"user \"{client.username}\" has joined")
65+
66+
proc processMessage(server: Server; client: Client; message: string) {.async.} =
67+
let parsed = parseMessage(message)
68+
echo client, " sent: ", message
69+
70+
case parsed.mtype
71+
of TextMessage:
72+
if verifyMessage(parsed):
73+
await relayMessage(server, message & "\c\l", skip=client.id)
74+
else:
75+
await sendWarning(client, "Message hash verification failed.")
76+
of ConnectionMessage:
77+
if parsed.text == "login":
78+
await handleLogin(server, client, parsed)
79+
else:
80+
discard
81+
82+
proc processClient(server: Server, client: Client) {.async.} =
83+
while true:
84+
let line = await client.socket.recvLine()
85+
if line.len == 0:
86+
if client.username != "":
87+
await server.broadcast(&"user \"{client.username}\" disconnected", skip=client.id)
88+
89+
disconnect(client)
90+
return
91+
92+
await processMessage(server, client, line)
93+
94+
proc loop(server: Server) {.async.} =
95+
while true:
96+
let (netAddr, clientSocket) = await server.socket.acceptAddr()
97+
let client = Client(
98+
socket: clientSocket,
99+
netAddr: netAddr,
100+
connected: false,
101+
id: server.clients.len
102+
)
103+
server.clients.add(client)
104+
asyncCheck server.processClient(client)
105+
echo "Accepted connection from ", netAddr, ", client id: ", client.id
106+
107+
proc main() =
108+
let server = newServer()
109+
var port = 7687
110+
111+
if paramCount() == 1:
112+
port = paramStr(1).parseInt
113+
114+
server.socket.bindAddr(Port(port))
115+
server.socket.listen()
116+
waitFor server.loop()
117+
118+
main()

0 commit comments

Comments
 (0)