├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin
├── client.py
└── client_curses.py
├── build.lisp
├── lisp-chat.asd
├── lisp-chat.png
├── logo
├── horizontal.png
├── logo.png
└── vertical.png
├── roswell
├── lisp-chat-server.ros
└── lisp-chat.ros
└── src
├── client.lisp
├── config.lisp
└── server.lisp
/.gitignore:
--------------------------------------------------------------------------------
1 | *.fasl
2 | *.FASL
3 | __pycache__
4 | system-index.txt
5 | lisp-chat
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM commonlispbr/roswell:latest
2 | RUN apt update && apt install libreadline7 -y
3 | WORKDIR /lisp-chat
4 | COPY ./lisp-chat.asd lisp-chat.asd
5 | COPY ./src src
6 | COPY ./roswell roswell
7 | RUN ros install ./
8 | EXPOSE 5558
9 | ENTRYPOINT ["/root/.roswell/bin/lisp-chat-server"]
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 Manoel Vilela
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SBCL_CMD := sbcl --noinform --disable-debugger --load
2 | OBJECTS := lisp-inference
3 |
4 |
5 | all: $(OBJECTS)
6 |
7 |
8 | $(OBJECTS): src/*.lisp
9 | $(SBCL_CMD) build.lisp
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [](http://quickdocs.org/lisp-chat/)
7 |
8 | # Lisp Chat
9 |
10 | An experimental chat irc-like written in Lisp.
11 |
12 | 
13 |
14 |
15 | # Installation
16 | Install [roswell][ros] and add `~/.roswell/bin/` to the `PATH` variable.
17 |
18 | After that just type:
19 |
20 | ``` bash
21 | ros install ryukinix/lisp-chat
22 | ```
23 |
24 | Lisp-chat it's on Quicklisp as well, tested on the following
25 | implementations:
26 |
27 | * SBCL
28 | * CCL
29 | * ECL
30 |
31 | # Usage
32 |
33 |
34 | Load the server
35 | ```bash
36 | $ lisp-chat-server localhost
37 | ```
38 |
39 | Create a client
40 | ```bash
41 | $ lisp-chat localhost
42 | ```
43 |
44 | As alternative of `localhost` you can use `server.lerax.me` which
45 | points to a lisp-chat server running in the port 5558.
46 |
47 |
48 |
49 | # Alternative clients
50 |
51 | If you want test this and don't have the Lisp requested, I have those
52 | alternatives for you:
53 |
54 | * Terminal text-based python client
55 | * Terminal ncurses python client
56 | * Netcat client (wtf?)
57 |
58 | On Python client, I wrote in a way only using ths stdlib avoiding pain
59 | to handle the dependency hell, so you can just call that:
60 |
61 | ```bash
62 | $ python client.py
63 | ```
64 |
65 | So finally... netcat. Yes! You can even just use `netcat`! An user
66 | called `Chris` in past days just logged in the server with the
67 | following message:
68 |
69 | ```
70 | |16:30:37| [Chris]: Used netcad
71 | |16:30:41| [Chris]: netcat*
72 | |16:30:50| [Chris]: bye
73 | ```
74 |
75 | So you can type `netcat server.lerax.me 5558` and go on! I tested on
76 | my machine and works fine! The main reason is because the
77 | communication between server and client just use raw data. For better
78 | synchronization with text data from server while you typing, I suggest
79 | you to use a readline wrapper like
80 | [`rlwrap`](https://github.com/hanslub42/rlwrap) calling as `rlwrap
81 | netcat server.lerax.me 5558`.
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/bin/client.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Author: Manoel Vilela
3 |
4 | import socket
5 | import threading
6 | import readline
7 |
8 | HOST = 'chat.lerax.me'
9 | PORT = 5558
10 |
11 | # set editing-mode on input
12 | readline.parse_and_bind('set editing-mode vi')
13 |
14 | s = socket.socket()
15 | s.connect((HOST, PORT))
16 | read_stream = s.makefile()
17 | write_stream = s.makefile(mode='w')
18 |
19 |
20 | def get_user_input():
21 | inp = input()
22 | print("\033[1A\033[2K", end='')
23 | return inp
24 |
25 |
26 | def server_reader():
27 | """Fetch lines from server and print"""
28 | try:
29 | while not read_stream.closed:
30 | print(read_stream.readline(), end='')
31 | except ValueError:
32 | pass
33 | # after the read_stream is closed, in the case
34 | # the thread is running read_stream.readline()
35 | # is running this will throw a exception of
36 | # IO operation on closed file.
37 |
38 |
39 | def send_message(message):
40 | write_stream.write(message + '\n')
41 | write_stream.flush()
42 |
43 |
44 | def main():
45 | try:
46 | username = input(str(read_stream.readline().strip('\n')))
47 | print("Connected as {}@{}:{}".format(username, HOST, PORT))
48 | send_message(username)
49 | inp = ""
50 | t = threading.Thread(target=server_reader)
51 | t.start()
52 | while inp != "/quit":
53 | inp = get_user_input()
54 | send_message(inp)
55 | except KeyboardInterrupt:
56 | send_message("/quit")
57 | finally:
58 | read_stream.close()
59 | write_stream.close()
60 | s.close()
61 | t.join()
62 |
63 |
64 | if __name__ == '__main__':
65 | main()
66 |
--------------------------------------------------------------------------------
/bin/client_curses.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Author: Manoel Vilela
3 |
4 | import curses
5 | import socket
6 | import threading
7 |
8 | HOST = 'chat.lerax.me'
9 | PORT = 5558
10 |
11 | s = socket.socket()
12 | s.connect((HOST, PORT))
13 | read_stream = s.makefile()
14 | write_stream = s.makefile(mode='w')
15 |
16 |
17 | class ChatUI(object):
18 | def __init__(self, stdscr):
19 | curses.use_default_colors()
20 | for i in range(0, curses.COLORS):
21 | curses.init_pair(i, i, -1)
22 | self.stdscr = stdscr
23 | self.inputbuffer = ""
24 | self.linebuffer = []
25 | self.chatbuffer = []
26 |
27 | # Curses, why must you confuse me with your height, width, y, x
28 | chatbuffer_hwyx = (curses.LINES - 2, curses.COLS-1, 0, 1)
29 | chatline_yx = (curses.LINES - 1, 0)
30 | self.win_chatline = stdscr.derwin(*chatline_yx)
31 | self.win_chatbuffer = stdscr.derwin(*chatbuffer_hwyx)
32 | self.redraw_ui()
33 |
34 | def resize(self):
35 | """Handles a change in terminal size"""
36 | h, w = self.stdscr.getmaxyx()
37 |
38 | self.win_chatline.mvwin(h - 1, 0)
39 | self.win_chatline.resize(1, w)
40 | self.win_chatbuffer.resize(h - 2, w - 2)
41 |
42 | self.linebuffer = []
43 | for msg in self.chatbuffer:
44 | self._linebuffer_add(msg)
45 |
46 | self.redraw_ui()
47 |
48 | def redraw_ui(self):
49 | """Redraws the entire UI"""
50 | h, w = self.stdscr.getmaxyx()
51 | self.stdscr.clear()
52 | self.stdscr.vline(0, 1, "|", h - 2)
53 | self.stdscr.hline(h - 2, 0, "-", w)
54 | self.stdscr.refresh()
55 |
56 | self.redraw_chatbuffer()
57 | self.redraw_chatline()
58 |
59 | def redraw_chatline(self):
60 | """Redraw the user input textbox"""
61 | h, w = self.win_chatline.getmaxyx()
62 | self.win_chatline.clear()
63 | start = len(self.inputbuffer) - w + 1
64 | if start < 0:
65 | start = 0
66 | self.win_chatline.addstr(0, 0, self.inputbuffer[start:])
67 | self.win_chatline.refresh()
68 |
69 | def redraw_chatbuffer(self):
70 | """Redraw the chat message buffer"""
71 | self.win_chatbuffer.clear()
72 | h, w = self.win_chatbuffer.getmaxyx()
73 | j = len(self.linebuffer) - h
74 | if j < 0:
75 | j = 0
76 | for i in range(min(h, len(self.linebuffer))):
77 | self.win_chatbuffer.insstr(i, 0, self.linebuffer[j])
78 | j += 1
79 | self.win_chatbuffer.refresh()
80 |
81 | def chatbuffer_add(self, msg):
82 | """Add a message to the chat buffer, automatically slicing it to
83 | fit the width of the buffer
84 | """
85 | self.chatbuffer.append(msg)
86 | self._linebuffer_add(msg)
87 | self.redraw_chatbuffer()
88 | self.redraw_chatline()
89 | self.win_chatline.cursyncup()
90 |
91 | def _linebuffer_add(self, msg):
92 | h, w = self.stdscr.getmaxyx()
93 | w = w - 2
94 | while len(msg) >= w:
95 | self.linebuffer.append(msg[:w])
96 | msg = msg[w:]
97 | if msg:
98 | self.linebuffer.append(msg)
99 |
100 | def prompt(self, msg):
101 | """Prompts the user for input and returns it"""
102 | self.inputbuffer = msg
103 | self.redraw_chatline()
104 | res = self.wait_input()
105 | res = res[len(msg):]
106 | return res
107 |
108 | def wait_input(self, prompt=""):
109 | """Wait for the user to input a message and hit enter.
110 | Returns the message
111 | """
112 | self.inputbuffer = prompt
113 | self.redraw_chatline()
114 | self.win_chatline.cursyncup()
115 | last = -1
116 | while last != ord('\n'):
117 | last = self.stdscr.getch()
118 | if last == ord('\n'):
119 | tmp = self.inputbuffer
120 | self.inputbuffer = ""
121 | self.redraw_chatline()
122 | self.win_chatline.cursyncup()
123 | return tmp[len(prompt):]
124 | elif last == curses.KEY_BACKSPACE or last == 127:
125 | if len(self.inputbuffer) > len(prompt):
126 | self.inputbuffer = self.inputbuffer[:-1]
127 | elif last == curses.KEY_RESIZE:
128 | self.resize()
129 | elif 32 <= last <= 126:
130 | self.inputbuffer += chr(last)
131 | self.redraw_chatline()
132 |
133 |
134 | def writer():
135 | """Fetch lines from server and fill the chatbuffer"""
136 | global chat
137 | while not read_stream.closed:
138 | chat.chatbuffer_add(read_stream.readline())
139 |
140 |
141 | def main(stdscr):
142 | """Main routine to be wrapped on curses.wrapper"""
143 | global chat
144 | stdscr.clear()
145 | chat = ChatUI(stdscr)
146 | try:
147 | username = chat.wait_input(str(read_stream.readline().strip('\n')))
148 | write_stream.write(username + '\n')
149 | write_stream.flush()
150 | inp = ""
151 | t = threading.Thread(target=writer)
152 | t.start()
153 | while inp != "/quit":
154 | inp = chat.wait_input()
155 | write_stream.write(inp + '\n')
156 | write_stream.flush()
157 | except KeyboardInterrupt:
158 | pass
159 | finally:
160 | read_stream.close()
161 | write_stream.close()
162 | s.close()
163 | t.join()
164 |
165 |
166 | curses.wrapper(main)
167 |
--------------------------------------------------------------------------------
/build.lisp:
--------------------------------------------------------------------------------
1 | (defparameter *compression* 9 "Compression level of the executable binary.")
2 | (defparameter *debug* nil "Debug information")
3 |
4 | (eval-when (:execute)
5 | (pushnew (truename (sb-unix:posix-getcwd/)) ql:*local-project-directories*)
6 | (ql:register-local-projects)
7 | (ql:quickload '(:lisp-chat :cffi)))
8 |
9 | (defmacro debug-format (&rest body)
10 | (when *debug*
11 | `(format t ,@body)))
12 |
13 | (defun import-foreign-libraries ()
14 | (let ((libpath (uiop/os:getcwd)))
15 | (debug-format "=> New LD_LIBRARY_PATH: ~a~%" libpath)
16 | (pushnew libpath
17 | cffi:*foreign-library-directories*
18 | :test #'equal)
19 | (cffi:define-foreign-library libreadline
20 | (t "libreadline.so"))
21 | (cffi:use-foreign-library libreadline))
22 | (debug-format "Loaded libraries: ~a~%" (cffi:list-foreign-libraries)))
23 |
24 |
25 | (defun main()
26 | (import-foreign-libraries)
27 | (lisp-chat/client:main))
28 |
29 |
30 | (eval-when (:execute)
31 | ;; close currently foreign libraries loaded
32 | (loop for library in (cffi:list-foreign-libraries :loaded-only t)
33 | do (cffi:close-foreign-library library))
34 | (sb-ext:save-lisp-and-die "lisp-chat"
35 | :toplevel #'main
36 | :executable t
37 | :compression *compression*))
38 |
--------------------------------------------------------------------------------
/lisp-chat.asd:
--------------------------------------------------------------------------------
1 | ;; Common Lisp Script
2 | ;; Manoel Vilela
3 |
4 |
5 | (asdf/defsystem:defsystem :lisp-chat/server
6 | :author "Manoel Vilela"
7 | :description "An experimental chat irc-like: server"
8 | :version "0.2.0"
9 | :mailto "manoel_vilela@engineer.com"
10 | :license "MIT"
11 | :depends-on ("usocket"
12 | "bordeaux-threads")
13 | :pathname "src"
14 | :components ((:file "config")
15 | (:file "server" :depends-on ("config"))))
16 |
17 | (asdf/defsystem:defsystem :lisp-chat/client
18 | :author "Manoel Vilela"
19 | :description "An experimental chat irc-like: client"
20 | :version "0.2.0"
21 | :license "MIT"
22 | :depends-on ("usocket"
23 | "cl-readline"
24 | "bordeaux-threads")
25 | :pathname "src"
26 | :components ((:file "config")
27 | (:file "client" :depends-on ("config"))))
28 |
29 | (asdf/defsystem:defsystem :lisp-chat
30 | :author "Manoel Vilela"
31 | :description "An experimental chat irc-like"
32 | :version "0.2.0"
33 | :license "MIT"
34 | :depends-on ("lisp-chat/client"
35 | "lisp-chat/server"))
36 |
--------------------------------------------------------------------------------
/lisp-chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryukinix/lisp-chat/c60dea5e94acb2ae0858587204e77aa0a9ad486b/lisp-chat.png
--------------------------------------------------------------------------------
/logo/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryukinix/lisp-chat/c60dea5e94acb2ae0858587204e77aa0a9ad486b/logo/horizontal.png
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryukinix/lisp-chat/c60dea5e94acb2ae0858587204e77aa0a9ad486b/logo/logo.png
--------------------------------------------------------------------------------
/logo/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryukinix/lisp-chat/c60dea5e94acb2ae0858587204e77aa0a9ad486b/logo/vertical.png
--------------------------------------------------------------------------------
/roswell/lisp-chat-server.ros:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #|-*- mode:lisp -*-|#
3 | #|
4 | exec ros -Q -- $0 "$@"
5 | |#
6 | (progn ;;init forms
7 | (ros:ensure-asdf)
8 | #+quicklisp(ql:quickload '(:lisp-chat/server) :silent t))
9 |
10 | (defpackage :ros.script.lisp-chat-server
11 | (:use :cl))
12 | (in-package :ros.script.lisp-chat-server)
13 |
14 |
15 | (defparameter *usage*
16 | (format nil
17 | "usage: lisp-chat-server [-h|--help] [host] [port]
18 |
19 | OPTIONS
20 | host server host, default: ~a
21 | port server port, default: ~a
22 | -h | --help show this message
23 | " lisp-chat/config:*host*
24 | lisp-chat/config:*port*))
25 |
26 | (defun help (argv)
27 | (when (loop for arg in argv
28 | thereis (or (equal arg "-h")
29 | (equal arg "--help")))
30 | (princ *usage*)))
31 |
32 | (defun main (&rest argv)
33 | (declare (ignorable argv))
34 |
35 | (let ((host (car argv))
36 | (port (cadr argv)))
37 | (when host
38 | (setq lisp-chat/config:*host* host))
39 | (when port
40 | (setq lisp-chat/config:*port* (parse-integer port))))
41 |
42 | (unless (help argv)
43 | (lisp-chat/server:main :host lisp-chat/config:*host*
44 | :port lisp-chat/config:*port*)))
45 | ;;; vim: set ft=lisp lisp:
46 |
--------------------------------------------------------------------------------
/roswell/lisp-chat.ros:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #|-*- mode:lisp -*-|#
3 | #|
4 | exec ros -Q -- $0 "$@"
5 | |#
6 | (progn ;;init forms
7 | (ros:ensure-asdf)
8 | #+quicklisp (ql:quickload '(lisp-chat/client) :silent t))
9 |
10 | (defpackage :ros.script.lisp-chat
11 | (:use :cl))
12 | (in-package :ros.script.lisp-chat)
13 |
14 | (defparameter *usage*
15 | (format nil
16 | "usage: lisp-chat [-h|--help] [host] [port]
17 |
18 | OPTIONS
19 | host server host, default: ~a
20 | port server port, default: ~a
21 | -h | --help show this message
22 | " lisp-chat/config:*host*
23 | lisp-chat/config:*port*))
24 |
25 | (defun help (argv)
26 | (when (loop for arg in argv
27 | thereis (or (equal arg "-h")
28 | (equal arg "--help")))
29 | (princ *usage*)))
30 |
31 |
32 | (defun main (&rest argv)
33 | (declare (ignorable argv))
34 |
35 | (let ((host (car argv))
36 | (port (cadr argv)))
37 | (when host
38 | (setq lisp-chat/config:*host* host))
39 | (when port
40 | (setq lisp-chat/config:*port* (parse-integer port))))
41 |
42 | (unless (help argv)
43 | (lisp-chat/client:main :host lisp-chat/config:*host*
44 | :port lisp-chat/config:*port*)))
45 | ;;; vim: set ft=lisp lisp:
46 |
--------------------------------------------------------------------------------
/src/client.lisp:
--------------------------------------------------------------------------------
1 | ;; Common Lisp Script
2 | ;; Manoel Vilela
3 |
4 | (defpackage :lisp-chat/client
5 | (:use #:usocket
6 | #:cl
7 | #:lisp-chat/config
8 | #:bordeaux-threads)
9 | (:export :main))
10 |
11 | (in-package :lisp-chat/client)
12 |
13 | (defvar *io-lock* (make-lock "io mutex")
14 | "I/O Mutex for avoid terminal race conditions")
15 |
16 | (defun erase-last-line ()
17 | "Erase the last line by using ANSI Escape codes"
18 | (format t "~C[1A~C[2K" #\Esc #\Esc))
19 |
20 |
21 | (defun get-user-input (username)
22 | "Get the user input by using readline"
23 | (prog1 (cl-readline:readline :prompt (format nil "[~A]: " username)
24 | :erase-empty-line t
25 | :add-history t)
26 | (with-lock-held (*io-lock*)
27 | (erase-last-line))))
28 |
29 |
30 | (defun send-message (message socket)
31 | "Send a MESSAGE string through a SOCKET instance"
32 | (write-line message (socket-stream socket))
33 | (finish-output (socket-stream socket)))
34 |
35 | ;; HACK: I don't know a better way to save state of cl-readline
36 | ;; before printing messages from server, so I'm cleaning all the stuff
37 | ;; before print a new message, and restore again. Maybe there is a
38 | ;; better way for doing that.
39 | (defun receive-message (message)
40 | "Receive a message and print in the terminal carefully with IO race conditions"
41 | (with-lock-held (*io-lock*)
42 | (let ((line cl-readline:*line-buffer*)
43 | (prompt cl-readline:+prompt+))
44 | ;; erase
45 | (cl-readline:replace-line "" nil)
46 | (cl-readline:set-prompt "")
47 | (cl-readline:redisplay)
48 | ;; print message from server
49 | (write-line message)
50 | ;; restore
51 | (cl-readline:replace-line (or line "") nil)
52 | (setq cl-readline:*point* cl-readline:+end+)
53 | (cl-readline:set-prompt prompt)
54 | (cl-readline:redisplay))))
55 |
56 | (defun client-sender (socket username)
57 | "Routine to check new messages being typed by the user"
58 | (loop for message = (get-user-input username)
59 | when (or (equal message "/quit")
60 | (equal message nil))
61 | return nil
62 | do (send-message message socket))
63 | (uiop:quit))
64 |
65 |
66 | (defun server-listener (socket)
67 | "Routine to check new messages coming from the server"
68 | (loop for message = (read-line (socket-stream socket))
69 | while (not (equal message "/quit"))
70 | do (receive-message message)))
71 |
72 | (defun server-broadcast (socket)
73 | "Call server-listener treating exceptional cases"
74 | (handler-case (server-listener socket)
75 | (end-of-file (e)
76 | (format t "~%End of connection: ~a~%" e)
77 | (uiop:quit 1))
78 | (simple-error ()
79 | (server-broadcast socket))))
80 |
81 |
82 | (defun login (socket)
83 | "Do the login of the application given a SOCKET instances"
84 | (princ (read-line (socket-stream socket)))
85 | (finish-output)
86 | (let ((username (read-line)))
87 | (send-message username socket)
88 | username))
89 |
90 |
91 | (defun client-loop (host port)
92 | "Dispatch client threads for basic functioning system"
93 | (let* ((socket (socket-connect host port))
94 | (username (login socket)))
95 | (format t "Connected as ~a\@~a\:~a ~%" username *host* *port*)
96 | (let ((sender (make-thread (lambda () (client-sender socket username))
97 | :name "client sender"))
98 | (broadcast (make-thread (lambda () (server-broadcast socket))
99 | :name "server broadcast")))
100 | (join-thread sender)
101 | (join-thread broadcast))))
102 |
103 | (defun main (&key (host *host*) (port *port*))
104 | "Main function of client"
105 | (handler-case (client-loop host port)
106 | (#+sbcl sb-sys:interactive-interrupt
107 | #+ccl ccl:interrupt-signal-condition
108 | #+clisp system::simple-interrupt-condition
109 | #+ecl ext:interactive-interrupt
110 | #+allegro excl:interrupt-signal ()
111 | (uiop:quit 0))
112 | (usocket:connection-refused-error ()
113 | (progn (write-line "Server seems offline. Run first the server.lisp.")
114 | (uiop:quit 1)))))
115 |
--------------------------------------------------------------------------------
/src/config.lisp:
--------------------------------------------------------------------------------
1 | ;; Common Lisp Script
2 | ;; Manoel Vilela
3 |
4 | (defpackage :lisp-chat/config
5 | (:use :cl)
6 | (:export :*debug*
7 | :*host*
8 | :*port*))
9 |
10 | (in-package :lisp-chat/config)
11 |
12 | (defparameter *debug* t "Run application in debug mode with extra info in terminal")
13 | (defparameter *host* "localhost" "Host used in server and client")
14 | (defparameter *port* 5558 "Default port")
15 |
--------------------------------------------------------------------------------
/src/server.lisp:
--------------------------------------------------------------------------------
1 | ;; Common Lisp Script
2 | ;; Manoel Vilela
3 |
4 | (defpackage #:lisp-chat/server
5 | (:use #:usocket
6 | #:cl
7 | #:lisp-chat/config
8 | #:bordeaux-threads)
9 | (:export #:main))
10 |
11 | (in-package :lisp-chat/server)
12 |
13 |
14 | ;; global vars
15 | (defvar *day-names* '("Monday" "Tuesday" "Wednesday"
16 | "Thursday" "Friday" "Saturday" "Sunday")
17 | "Day names")
18 | (defvar *uptime* (multiple-value-list (get-decoded-time))
19 | "Uptime of server variable")
20 | (defparameter *commands-names*
21 | '("/users" "/help" "/log" "/quit" "/uptime" "/nick")
22 | "Allowed command names to be called by client user")
23 | (defparameter *clients* nil "List of clients")
24 | (defparameter *messages-stack* nil "Messages pending to be send by broadcasting")
25 | (defparameter *messages-log* nil "Messages log")
26 | (defparameter *server-nickname* "@server" "The server nickname")
27 |
28 |
29 | ;; thread control
30 | (defvar *message-semaphore* (make-semaphore :name "message semaphore"
31 | :count 0))
32 | (defvar *client-lock* (make-lock "client list lock"))
33 |
34 |
35 |
36 | (defstruct message
37 | "This structure abstract the type message with is saved
38 | into *messages-log* and until consumed, temporally pushed
39 | to *messages-stack*. FROM, CONTENT and TIME has type string"
40 | from
41 | content
42 | time )
43 |
44 | (defstruct client
45 | "This structure handle the creation/control of the clients of the server.
46 | NAME is a string. Socket is a USOCKET:SOCKET and address is a ipv4 encoded
47 | string. "
48 | name
49 | socket
50 | address)
51 |
52 |
53 | (defun socket-peer-address (socket)
54 | "Given a USOCKET:SOCKET instance return a ipv4 encoded IP string"
55 | (format nil "~{~a~^.~}\:~a"
56 | (map 'list #'identity (get-peer-address socket))
57 | (get-peer-port socket)))
58 |
59 | (defun client-stream (c)
60 | "Select the stream IO from the client"
61 | (socket-stream (client-socket c)))
62 |
63 |
64 | (defun debug-format (&rest args)
65 | "If *debug* from lisp-chat-config is true, print debug info on
66 | running based on ARGS"
67 | (if *debug*
68 | (apply #'format args)))
69 |
70 |
71 | (defun get-time ()
72 | "Return a encoded string as HH:MM:SS based on the current timestamp."
73 | (multiple-value-bind (second minute hour)
74 | (get-decoded-time)
75 | (format nil "~2,'0d:~2,'0d:~2,'0d" hour minute second)))
76 |
77 |
78 | (defun formated-message (message)
79 | "The default message format of this server. MESSAGE is a string
80 | Changing this reflects all the layout from client/server.
81 | Probably this would be the MFRP: Manoel Fucking Raw Protocol.
82 | Because this we can still use netcat as client for lisp-chat."
83 | (format nil "|~a| [~a]: ~a"
84 | (message-time message)
85 | (message-from message)
86 | (message-content message)))
87 |
88 | (defun command-message (content)
89 | "This function prepare the CONTENT as a message by the @server"
90 | (let* ((from *server-nickname*)
91 | (time (get-time))
92 | (message (make-message :from from :content content :time time)))
93 | (formated-message message)))
94 |
95 | (defun call-command-by-name (string params)
96 | "Wow, this is a horrible hack to get a string as symbol for functions/command
97 | like /help /users /log and so on."
98 | (let ((command-function (find-symbol (string-upcase string) :lisp-chat/server)))
99 | (when command-function
100 | (apply command-function params))))
101 |
102 | ;; user commands prefixed with /
103 | (defun /users (client &rest args)
104 | "Return a list separated by commas of the currently logged users"
105 | (declare (ignorable client args))
106 | (command-message (format nil "~{~a~^, ~}" (mapcar #'client-name *clients*))))
107 |
108 |
109 | (defun /help (client &rest args)
110 | "Show a list of the available commands of lisp-chat"
111 | (declare (ignorable client args))
112 | (command-message (format nil "~{~a~^, ~}" *commands-names*)))
113 |
114 |
115 | (defun /log (client &optional (depth "20") &rest args)
116 | "Show the last messages typed on the server.
117 | DEPTH is optional number of messages frames from log"
118 | (declare (ignorable client args))
119 | (let ((messages (min (or (parse-integer depth :junk-allowed t) 20)
120 | (length *messages-log*))))
121 | (format nil "~{~a~^~%~}" (reverse (subseq *messages-log* 0
122 | messages)))))
123 |
124 | (defun /uptime (client &rest args)
125 | "Return a string nice encoded to preset the uptime since the server started."
126 | (declare (ignorable client args))
127 | (multiple-value-bind
128 | (second minute hour date month year day-of-week dst-p tz)
129 | (values-list *uptime*)
130 | (declare (ignore dst-p))
131 | (command-message
132 | (format nil
133 | "Server online since ~2,'0d:~2,'0d:~2,'0d of ~a, ~2,'0d/~2,'0d/~d (GMT~@d)"
134 | hour minute second
135 | (nth day-of-week *day-names*)
136 | month date year
137 | (- tz)))))
138 |
139 | (defun /nick (client &optional (new-nick nil) &rest args)
140 | "Change the client-name given a NEW-NICK which should be a string"
141 | (declare (ignorable args))
142 | (if new-nick
143 | (progn (setf (client-name client) new-nick)
144 | (command-message (format nil "Your new nick is: ~a" new-nick)))
145 | (command-message (format nil "/nick "))))
146 |
147 |
148 | (defun push-message (from content)
149 | "Push a messaged FROM as CONTENT into the *messages-stack*"
150 | (push (make-message :from from
151 | :content content
152 | :time (get-time))
153 | *messages-stack*)
154 | (signal-semaphore *message-semaphore*))
155 |
156 | (defun client-delete (client)
157 | "Delete a CLIENT from the list *clients*"
158 | (with-lock-held (*client-lock*)
159 | (setf *clients* (remove-if (lambda (c)
160 | (equal (client-address c)
161 | (client-address client)))
162 | *clients*)))
163 | (push-message "@server" (format nil "The user ~s exited from the party :("
164 | (client-name client)))
165 | (debug-format t "Deleted user ~a@~a~%"
166 | (client-name client)
167 | (client-address client))
168 | (socket-close (client-socket client)))
169 |
170 | (defun send-message (client message)
171 | "Send to CLIENT a MESSAGE :type string"
172 | (let ((stream (client-stream client)))
173 | (write-line message stream)
174 | (finish-output stream)))
175 |
176 | (defun startswith (string substring)
177 | "Check if STRING starts with SUBSTRING."
178 | (let ((l1 (length string))
179 | (l2 (length substring)))
180 | (when (and (> l2 0)
181 | (>= l1 l2))
182 | (loop for c1 across string
183 | for c2 across substring
184 | always (equal c1 c2)))))
185 |
186 | (defun split (string delimiterp)
187 | "Split a string by a delimiterp function character checking"
188 | (loop for beg = (position-if-not delimiterp string)
189 | then (position-if-not delimiterp string :start (1+ end))
190 | for end = (and beg (position-if delimiterp string :start beg))
191 | when beg
192 | collect (subseq string beg end)
193 | while end))
194 |
195 | (defun extract-params (string)
196 | (subseq (split string (lambda (c) (eql c #\Space)))
197 | 1))
198 |
199 | (defun call-command (client message)
200 | (let ((command (find message *commands-names* :test #'startswith)))
201 | (when command
202 | (call-command-by-name command (cons client
203 | (extract-params message))))))
204 |
205 | (defun client-reader-routine (client)
206 | "This function create a IO-bound procedure to act
207 | by reading the events of a specific CLIENT.
208 | On this software each client talks on your own thread."
209 | (loop for message := (read-line (client-stream client))
210 | while (not (equal message "/quit"))
211 | for response := (call-command client message)
212 | if response
213 | do (send-message client response)
214 | else
215 | when (> (length message) 0)
216 | do (push-message (client-name client)
217 | message)
218 | finally (client-delete client)))
219 |
220 | (defun client-reader (client)
221 | "This procedure is a wrapper for CLIENT-READER-ROUTINE
222 | treating all the possible errors based on HANDLER-CASE macro."
223 | (handler-case (client-reader-routine client)
224 | (end-of-file () (client-delete client))
225 | (#+sbcl sb-int:simple-stream-error
226 | #-sbcl error
227 | ()
228 | (progn (debug-format t "~a@~a timed output"
229 | (client-name client)
230 | (client-address client))
231 | (client-delete client)))
232 | (#+sbcl sb-bsd-sockets:not-connected-error
233 | #-sbcl error
234 | ()
235 | (progn (debug-format t "~a@~a not connected more."
236 | (client-name client)
237 | (client-address client))
238 | (client-delete client)))))
239 |
240 | (defun create-client (connection)
241 | "This procedure create a new client based on CONNECTION made by
242 | USOCKET:SOCKET-ACCEPT. This shit create a lot of side effects as messages
243 | if the debug is on because this makes all the log stuff to make analysis"
244 | (debug-format t "Incoming connection from ~a ~%" (socket-peer-address connection))
245 | (let ((client-stream (socket-stream connection)))
246 | (write-line "> Type your username: " client-stream)
247 | (finish-output client-stream)
248 | (let ((client (make-client :name (read-line client-stream)
249 | :socket connection
250 | :address (socket-peer-address connection))))
251 | (with-lock-held (*client-lock*)
252 | (debug-format t "Added new user ~a@~a ~%"
253 | (client-name client)
254 | (client-address client))
255 | (push client *clients*))
256 | (push-message "@server" (format nil "The user ~s joined to the party!" (client-name client)))
257 | (make-thread (lambda () (client-reader client))
258 | :name (format nil "~a reader thread" (client-name client))))))
259 |
260 | ;; a function defined to handle the errors of client thread
261 | (defun safe-client-thread (connection)
262 | "This function is a wrapper for CREATE-CLIENT treating the
263 | exceptions."
264 | (handler-case (create-client connection)
265 | (end-of-file () nil)
266 | (usocket:address-in-use-error () nil)))
267 |
268 | (defun message-broadcast ()
269 | "This procedure is a general independent thread to run brodcasting
270 | all the clients when a message is ping on this server"
271 | (loop when (wait-on-semaphore *message-semaphore*)
272 | do (let ((message (formated-message (pop *messages-stack*))))
273 | (push message *messages-log*)
274 | (loop for client in *clients*
275 | do (handler-case (send-message client message)
276 | (#+sbcl sb-int:simple-stream-error
277 | #-sbcl error ()
278 | (client-delete client))
279 | (#+sbcl sb-bsd-sockets:not-connected-error
280 | #-sbcl error ()
281 | (client-delete client)))))))
282 |
283 | (defun connection-handler (socket-server)
284 | "This is a special thread just for accepting connections from SOCKET-SERVER
285 | and creating new clients from it."
286 | (loop for connection = (socket-accept socket-server)
287 | do (make-thread (lambda () (safe-client-thread connection))
288 | :name "create client")))
289 |
290 | (defun server-loop (socket-server)
291 | "This is the general server-loop procedure. Create the threads
292 | necessary for the basic working state of this chat. The main idea
293 | is creating a MESSAGE-BROADCAST procedure and CONNECTION-HANDLER
294 | procedure running as separated threads.
295 |
296 | The first procedure send always a new message too all clients
297 | defined on *clients* when *messages-semaphore* is signalized.
298 | The second procedure is a general connection-handler for new
299 | clients trying connecting to the server."
300 | (format t "Running server at ~a:~a... ~%" *host* *port*)
301 | (let* ((connection-thread (make-thread (lambda () (connection-handler socket-server))
302 | :name "Connection handler"))
303 | (broadcast-thread (make-thread #'message-broadcast
304 | :name "Message broadcast")))
305 | (join-thread connection-thread)
306 | (join-thread broadcast-thread)))
307 |
308 | (defun main (&key (host *host*) (port *port*))
309 | "Well, this function run all the necessary shits."
310 | (let ((socket-server nil)
311 | (error-code 0))
312 | (unwind-protect
313 | (handler-case
314 | (progn (setq socket-server (socket-listen host port))
315 | (server-loop socket-server))
316 | (usocket:address-in-use-error ()
317 | (format *error-output*
318 | "error: Address:port at ~a\:~a already busy.~%"
319 | *host*
320 | *port*)
321 | (setq error-code 1))
322 | (usocket:address-not-available-error ()
323 | (format *error-output*
324 | "error: There is no way to use ~a as host to run the server.~%"
325 | *host*)
326 | (setq error-code 2))
327 | (#+sbcl sb-sys:interactive-interrupt
328 | #+ccl ccl:interrupt-signal-condition
329 | #+clisp system::simple-interrupt-condition
330 | #+ecl ext:interactive-interrupt
331 | #+allegro excl:interrupt-signal ()
332 | (format t "~%Closing the server...~%")))
333 | (when socket-server
334 | (socket-close socket-server))
335 | (uiop:quit error-code))))
336 |
--------------------------------------------------------------------------------