Telematica: Started assignment 1 report.

parent 6e437807
all: docs
.PHONY: docs
docs:
$(MAKE) -C doc latex
$(MAKE) -C doc/_build/latex all-pdf
ln -sf doc/_build/latex/Chatz0r.pdf Chatz0r.pdf
...@@ -9,6 +9,8 @@ EAGAIN = 11 ...@@ -9,6 +9,8 @@ EAGAIN = 11
class ClientConnection(object, AsyncBase, asyncore.dispatcher): class ClientConnection(object, AsyncBase, asyncore.dispatcher):
"""
"""
def __init__(self): def __init__(self):
AsyncBase.__init__(self) AsyncBase.__init__(self)
......
...@@ -2,6 +2,10 @@ import asyncore, socket ...@@ -2,6 +2,10 @@ import asyncore, socket
from Queue import Queue from Queue import Queue
class AsyncBase(asyncore.dispatcher): class AsyncBase(asyncore.dispatcher):
"""
Base class of the asynchronous connection of the chat system. The server and
the client will use this base class to implement their connection.
"""
def __init__(self): def __init__(self):
self.send_queue = Queue() self.send_queue = Queue()
...@@ -11,8 +15,10 @@ class AsyncBase(asyncore.dispatcher): ...@@ -11,8 +15,10 @@ class AsyncBase(asyncore.dispatcher):
self.event_list = { } self.event_list = { }
def init_loop(self): def init_loop(self):
"""Initialise the asyncore loop (blocking).""" """
# Set a timeout of 0.1 seconds and use poll() when available. Initialise the asyncore loop (blocking). Set a timeout of 0.1 seconds
and use poll() when available.
"""
asyncore.loop(.1, True) asyncore.loop(.1, True)
def debug_log(self, msg): def debug_log(self, msg):
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
import curses import curses
class BaseBar(object): class BaseBar(object):
"""
Base class of the user inteface bar components.
"""
def __init__(self, main, top, left, width): def __init__(self, main, top, left, width):
# Reference to the main window # Reference to the main window
self.main = main self.main = main
...@@ -17,12 +21,21 @@ class BaseBar(object): ...@@ -17,12 +21,21 @@ class BaseBar(object):
self._prefix = '' self._prefix = ''
def prefix(self, value=None): def prefix(self, value=None):
"""
Change or get the prefix of the bar. If value is None, the current
prefix value is returned.
"""
if value == None: # Note: an empty string can also be set. if value == None: # Note: an empty string can also be set.
return self._prefix return self._prefix
self._prefix = value self._prefix = value
self.display(self._msg) self.display(self._msg)
def display(self, msg): def display(self, msg):
"""
Display a message on the bar. This will overwrite the currently
displayed message. However, this will not overwrite the prefix value and
this prefix value will be prepended to the message before displaying.
"""
self._msg = msg self._msg = msg
if self.prefix(): if self.prefix():
msg = self.prefix() + ' ' + msg msg = self.prefix() + ' ' + msg
...@@ -36,5 +49,8 @@ class BaseBar(object): ...@@ -36,5 +49,8 @@ class BaseBar(object):
self.refresh() self.refresh()
def refresh(self): def refresh(self):
"""
Refresh the content of the bar.
"""
self.bar.refresh() self.bar.refresh()
...@@ -3,6 +3,10 @@ from Queue import Queue ...@@ -3,6 +3,10 @@ from Queue import Queue
from time import strftime from time import strftime
class BaseWindow(object): class BaseWindow(object):
"""
Base class of the user interface window components.
"""
def __init__(self, main, top, left, height, width): def __init__(self, main, top, left, height, width):
# Reference to the main window # Reference to the main window
self.main = main self.main = main
...@@ -20,10 +24,20 @@ class BaseWindow(object): ...@@ -20,10 +24,20 @@ class BaseWindow(object):
self.window = curses.newwin(height, width, top, left) self.window = curses.newwin(height, width, top, left)
def clear(self): def clear(self):
"""
Clear the window object. This will hide all rows (but does not discard
the buffer containing all message).
"""
self.window.clear() self.window.clear()
self.refresh() self.refresh()
def redraw(self): def redraw(self):
"""
Clear the window object and draw the rows containing last appended
messages in the window object. If not all message fit in the window, the
last appended messages are shown (last appended message is displayed as
last message) and the messages which do not fit are not displayed.
"""
self.window.clear() self.window.clear()
while not self.line_queue.empty(): while not self.line_queue.empty():
...@@ -43,6 +57,10 @@ class BaseWindow(object): ...@@ -43,6 +57,10 @@ class BaseWindow(object):
self.window.refresh() self.window.refresh()
def append(self, msg): def append(self, msg):
"""
Add a message to the bottom of the window. This will move all messages
one row up.
"""
time_prefix = strftime('%H:%M') + ' ' time_prefix = strftime('%H:%M') + ' '
while msg: while msg:
msg = time_prefix + msg msg = time_prefix + msg
......
...@@ -69,10 +69,9 @@ class CLI: ...@@ -69,10 +69,9 @@ class CLI:
def init_windows(self): def init_windows(self):
""" """
Initialise the window of this command line interface. The user interface Initialise the window of this command line interface. The user interface
of this chat application consists of a main chat window, an optional of this chat application consists of a main chat window, a debug window,
user list window, an optional debug window, an info bar and at the an info bar and at the bottom a command bar. The chat window does also
bottom a command bar. The chat window does also view the help screen, view the help screen, when the help screen is requested by the user.
when the help screen is requested by the user.
""" """
self.max_y, self.max_x = self.stdscr.getmaxyx() self.max_y, self.max_x = self.stdscr.getmaxyx()
......
import socket
from Queue import Queue
import threading
import sys
import re
class Client:
"""
Chat client.
"""
def __init__(self, host, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.send_queue = Queue(['CHAT'])
self.connect(host, port)
self.sender = ClientThread(self)
self.sender.handle = ClientThread.send
self.sender.daemon = True
#self.sender.start()
self.receiver = ClientThread(self)
self.receiver.handle = ClientThread.receive
self.receiver.daemon = True
#self.receiver.start()
self.verified = False
# def __exit__(self):
# print 'End of program.'
#
# def __del__(self):
# sys.exit(0)
def connect(self, host, port):
self.sock.connect((host, port))
def parse(self, buf):
if not self.verified:
match = re.match('^CHAT/(\d\.\d)/([^\x00-\x1F/:]+)$', buf)
if match:
print 'Connected to server, server version is', match.group(1)
print match.group(2)
self.verified = True
else:
print 'Failed to verify connection'
sys.exit(-1)
else:
pass
class ClientThread(threading.Thread):
"""
Send/receive thread for chat client.
"""
def __init__(self, client):
threading.Thread.__init__(self)
self.client = client
def run(self):
print 'Starting new client thread.'
while True:
self.handle(self)
def send(self):
if self.client.send_queue.empty():
return
msg = self.client.send_queue.get()
print '> %s' % msg
msg += '\r\n'
msglen = len(msg)
totalsent = 0
while totalsent < msglen:
sent = self.client.sock.send(msg[totalsent:])
if not sent:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sent
def receive(self):
buf = ''
multi_line = False
response_format = False
while True:
chunk = self.client.sock.recv(1)
if not chunk:
raise RuntimeError("socket connection broken")
if chunk in ['-','+']:
response_format = True
elif chunk == ':':
multi_line = True
elif chunk == '\n' and buf[-1] == '\r':
if not multi_line or buf[-3:] == '\r\n\r':
break
multi_line = False
buf += chunk
print '< %s' % buf[:-1]
self.client.parse(buf[:-1])
class GUI(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.messages = Queue()
def run(self):
print 'Start reading input'
while True:
msg = sys.stdin.readline()
if not msg:
print 'ctrl-d'
sys.exit(-1)
break
msg = msg[:-1]
print msg
if msg == 'q':
print 'quitting'
sys.exit(-1)
break
def start(self, client):
self.client = client
threading.Thread.start(self)
#if __name__ == '__main__':
print 'Program started'
gui = GUI()
print 'GUI started'
client = Client('ow150.science.uva.nl', 16897)
gui.start(client)
gui.join()
...@@ -8,7 +8,7 @@ class CommandBar(BaseBar): ...@@ -8,7 +8,7 @@ class CommandBar(BaseBar):
""" """
Command bar is part of the Command Line Interface (CLI) of the chat Command bar is part of the Command Line Interface (CLI) of the chat
application. The command bar is responsible for handling the user input. application. The command bar is responsible for handling the user input.
This inclues executing commands and (re)drawing the command line. This includes executing commands and (re)drawing the command line.
""" """
def __init__(self, main, top, left, width): def __init__(self, main, top, left, width):
super(CommandBar, self).__init__(main, top, left, width) super(CommandBar, self).__init__(main, top, left, width)
......
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Chatz0r.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Chatz0r.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
Appendix
========
Appendix A: API documentation
-----------------------------
.. automodule:: asyncbase
:members:
.. automodule:: async
:members:
.. automodule:: base_bar
:members:
.. automodule:: base_window
:members:
.. automodule:: cli
:members:
.. automodule:: command_bar
:members:
.. automodule:: info_bar
:members:
.. automodule:: chat_window
:members:
.. automodule:: debug_window
:members:
.. automodule:: server
:members:
Appendix B: screenshots
-----------------------
Foo
Assignment
==========
Introduction and purpose
------------------------
This is the first assignment for the Telemetric course at the University of
Amsterdam. The assignment is to create a chat server and client according to a given
protocol. This protocol is specifically designed for this assignment and is
explained in detail in the "protocol" section.
Besides designing a chat system according to the given protocol, the server of
the chat system should be able to handle multiple clients at once. Optionally, a
graphical user interface can be used to control the client, as long as the
client runs on the Linux machines of the university.
Concept of the application
--------------------------
Given the constraints of the assignment, we designed a chat system with an
intuitive user interface used in the client and a scalable server to serve the
clients. We did not design a GUI for the server intentionally, since a server
should be able to run headless; server log files are all you need.
We chose to create a text-based interface instead of a graphical user interface
for the chat client. It is not explicitly mentioned in the assignment that the
user interface should be graphical or text-based (since there is no interface
mentioned at all). It only states it is optional to make the client "graphical".
Especially, if you run the chat client on a remote server, it is more efficient
to use a text-based interface. Running the chat client on a remote server will
allow the user to stay logged in to the chat channel and receive all messages,
while the user can turn off his computer during the night. This construction of
a remote client is also known as a *daemon* and is inspired by and largely used
in combination with the IRC [1]_ client irssi [2]_.
.. [1] Internet Relay Chat.
.. [2] Irssi chat client: http://irssi.org/.
Even though both authors of this chat system use Linux, we chose to create a
cross-platform application. Besides platform independence, we wanted to create a
robust and flexible chat system. Python is a language that suits these
requirements. We used the python bindings of *curses* [3]_ to implement the
text-based user interface of the client.
.. [3] curses -- Terminal handling for character-cell displays:
http://docs.python.org/library/curses.html.
# -*- coding: utf-8 -*-
#
# Chatz0r documentation build configuration file, created by
# sphinx-quickstart on Thu Mar 10 20:00:01 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('..'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.pngmath', 'sphinx.ext.autodoc', 'sphinx.ext.doctest',
'sphinx.ext.todo']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Chatz0r'
copyright = u'2011, Taddeüs Kroes, Sander van Veen'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'Chatz0rdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Chatz0r.tex', u'Chatz0r Documentation',
u'Taddeüs Kroes, Sander van Veen', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
Implementation details
======================
Client side
----------
Client chat connection
~~~~~~~~~~~~~~~~~~~~~~
Foo
Text-based interface (using curses)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The designed text-based user interface consists of four major components:
1. **Main chat window.** This window displays all notifications sent from the
server and will show all chat messages sent and received. This window is
shown at the top of the interface.
2. **Debug log window.** Information regarding the chat protocol, incoming and
outgoing protocol messages and raised exceptions are displayed in this
window. This window is displayed below the main chat window and above the
indicator bar.
3. **Indicator bar.** When the client raises an exception, the indicator bar
will display the exception's error code and message to the user. The
indicator bar does also show the connectivity (online, when connected to a
server, or offline) and if connected to a chat server, the currently used
nickname. This bar is shown below the debug log window and above the
command bar.
4. **Command bar.** The user uses this bar to control the chat client (using
special commands; see section "User interface commands" for these commands)
and send chat messages to the server. This command bar is shown at the
bottom of the interface.
The main chat window and debug log window have a common base class called
*BaseWindow*. This base class contains the initialisation of the curses window
object using the function *curses.newwin(height, width, top, left)*. The base
class takes care of redrawing the window object. Redrawing is necessary to
display new information (if a new line is appended to the window, the other
lines should move one line up) and hide old information (if there are more lines
than the window object's height).
The indicator bar and command bar have a common base class called *BaseBar*.
This base class initialises the window object of this bar similar to the
*BaseWindow* class (except that the height is always :math:`1`, see below why).
It also contains functionality to change the information currently displayed at
the bar and it allows to specify a prefix (which will remain the same if the
message of the bar is modified). The prefix is invented to simplify changing
the indicator bar (for example, to display an error message, if a non-fatal
error occurs) but prevent the change from overwriting all information shown at
the indicator bar (e.g. nickname and currently connected server).
We used a simple form of window composition. First, get the horizontal and
vertical maximum amount of characters the terminal can display, using the screen
object's function *getmaxyx()* (the y and x axis are swapped for historical
reasons). The height of a bar is one character row. Therefore, the command bar
at the bottom takes one row and the indicator bar above the command bar takes
another row. Above the indicator bar is the debug window, which height is five
rows (we used this fixed value because it worked well during the development of
the chat client). To separate the content of the debug window from the content
of the main chat window, we added an empty row between the two windows. After
obtaining the size of the terminal, the chat window's height is set to
:math:`y_{max} - 8` (where :math:`8` is the sum of the two bars, an empty row
and the debug window). The width of the windows and bars is set to
:math:`x_{max}`.
User interface commands
~~~~~~~~~~~~~~~~~~~~~~~
In order to control the chat client, we implemented some features which can be
executed using a special command. To execute a special command, you should start
your chat message with a forward-slash directly followed by the command to
execute. The forward-slash should be the first character of the command bar,
otherwise the command is not recognised (the slash distinguishes chat messages
from commands to execute).
We implemented the following commands:
* **/close** Close visible chat window or the help screen. In the future,
this will allow you to connect to multiple chat server and close an
individual chat screen (for example, when private messaging is implemented).
For now, this command will close the help screen or if no help screen is
displayed, shutdown the chat client application.
* **/connect** Connect to a chat server using **HOSTNAME** and optionally a
**PORT**. If no **PORT** is specified, the default port 16897 is used. This
command will first disconnect from any currently connected chat server.
Synopsis: **/connect HOSTNAME [PORT]**
For example, **/connect ow150** will connect to the provided test server of
this assignment (when you're using the UvA network).
* **/disconnect** Disconnect from the currently connected chat server. If you
are not connected to a chat server, this command will do nothing.
* **/exec** Execute python code. All following arguments are interpreted by the
python interpreter as if it was real python code. Therefore, you should pay
attention to the syntax and semantics. You can use the variable **main** to
access all other parts of the chat client (since it is the CLI instance).
For example, to list all variables and function of the CLI, type: **/exec
print dir(main)**.
* **/help** This help page.
* **/names** Retrieve and display a list of user names currently logged in on
the chat server.
* **/quit** Quit this chat application (shortcut: ^c).
* **/raw** Send a raw command to the server. This is useful during
debugging the client and/or server since you do not have to restart the
application to send an unimplemented message to the server/client. All
following arguments are sent as a literal string to the server or client.
* **/user** Change your user name to **USER**.
Synopsis: **/user USER**
Server side
----------
Foo
.. Chatz0r documentation master file, created by
sphinx-quickstart on Thu Mar 10 20:00:01 2011.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Chatz0r's documentation!
===================================
.. toctree::
:maxdepth: 3
assignment.rst
protocol.rst
implementation.rst
appendix.rst
Chat protocol
=============
Foo
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment