commit 97378998a5f8c031444fd7a0c1b1007e9282df4d Author: Lance Stout Date: Thu Jan 5 11:31:54 2012 -0500 Break the docs out into their own branch. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..a520f6a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,130 @@ +# 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 singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @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 " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @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." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +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/SleekXMPP.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SleekXMPP.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/SleekXMPP" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SleekXMPP" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +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." diff --git a/docs/_static/agogo.css b/docs/_static/agogo.css new file mode 100644 index 0000000..8cdbf9c --- /dev/null +++ b/docs/_static/agogo.css @@ -0,0 +1,452 @@ +/* + * agogo.css_t + * ~~~~~~~~~~~ + * + * Sphinx stylesheet -- agogo theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +* { + margin: 0px; + padding: 0px; +} + +body { + font-family: "Verdana", Arial, sans-serif; + line-height: 1.4em; + color: black; + background-color: #eeeeec; +} + + +/* Page layout */ + +div.header, div.content, div.footer { + width: 70em; + margin-left: auto; + margin-right: auto; +} + +div.header-wrapper { + background: url(bgtop.png) top left repeat-x; + border-bottom: 3px solid #2e3436; +} + + +/* Default body styles */ +a { + color: #ce5c00; +} + +div.bodywrapper a, div.footer a { + text-decoration: underline; +} + +.clearer { + clear: both; +} + +.left { + float: left; +} + +.right { + float: right; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +h1, h2, h3, h4 { + font-family: "Georgia", "Times New Roman", serif; + font-weight: normal; + color: #3465a4; + margin-bottom: .8em; +} + +h1 { + color: #204a87; +} + +h2 { + padding-bottom: .5em; + border-bottom: 1px solid #3465a4; +} + +a.headerlink { + visibility: hidden; + color: #dddddd; + padding-left: .3em; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +img { + border: 0; +} + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 2px 7px 1px 7px; + border-left: 0.2em solid black; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +/* Header */ + +div.header { + padding-top: 10px; + padding-bottom: 10px; +} + +div.header h1 { + font-family: "Georgia", "Times New Roman", serif; + font-weight: normal; + font-size: 180%; + letter-spacing: .08em; +} + +div.header h1 a { + color: white; +} + +div.header div.rel { + margin-top: 1em; +} + +div.header div.rel a { + color: #fcaf3e; + letter-spacing: .1em; + text-transform: uppercase; +} + +p.logo { + float: right; +} + +img.logo { + border: 0; +} + + +/* Content */ +div.content-wrapper { + background-color: white; + padding-top: 20px; + padding-bottom: 20px; +} + +div.document { + width: 50em; + float: left; +} + +div.body { + padding-right: 2em; + text-align: justify; +} + +div.document ul { + margin: 1.5em; + list-style-type: square; +} + +div.document dd { + margin-left: 1.2em; + margin-top: .4em; + margin-bottom: 1em; +} + +div.document .section { + margin-top: 1.7em; +} +div.document .section:first-child { + margin-top: 0px; +} + +div.document div.highlight { + padding: 3px; + background-color: #eeeeec; + border-top: 2px solid #dddddd; + border-bottom: 2px solid #dddddd; + margin-top: .8em; + margin-bottom: .8em; +} + +div.document h2 { + margin-top: .7em; +} + +div.document p { + margin-bottom: .5em; +} + +div.document li.toctree-l1 { + margin-bottom: 1em; +} + +div.document .descname { + font-weight: bold; +} + +div.document .docutils.literal { + background-color: #eeeeec; + padding: 1px; +} + +div.document .docutils.xref.literal { + background-color: transparent; + padding: 0px; +} + +div.document blockquote { + margin: 1em; +} + +div.document ol { + margin: 1.5em; +} + + +/* Sidebar */ + +div.sidebar { + width: 20em; + float: right; + font-size: .9em; +} + +div.sidebar a, div.header a { + text-decoration: none; +} + +div.sidebar a:hover, div.header a:hover { + text-decoration: underline; +} + +div.sidebar h3 { + color: #2e3436; + text-transform: uppercase; + font-size: 130%; + letter-spacing: .1em; +} + +div.sidebar ul { + list-style-type: none; +} + +div.sidebar li.toctree-l1 a { + display: block; + padding: 1px; + border: 1px solid #dddddd; + background-color: #eeeeec; + margin-bottom: .4em; + padding-left: 3px; + color: #2e3436; +} + +div.sidebar li.toctree-l2 a { + background-color: transparent; + border: none; + margin-left: 1em; + border-bottom: 1px solid #dddddd; +} + +div.sidebar li.toctree-l3 a { + background-color: transparent; + border: none; + margin-left: 2em; + border-bottom: 1px solid #dddddd; +} + +div.sidebar li.toctree-l2:last-child a { + border-bottom: none; +} + +div.sidebar li.toctree-l1.current a { + border-right: 5px solid #fcaf3e; +} + +div.sidebar li.toctree-l1.current li.toctree-l2 a { + border-right: none; +} + + +/* Footer */ + +div.footer-wrapper { + background: url(bgfooter.png) top left repeat-x; + border-top: 4px solid #babdb6; + padding-top: 10px; + padding-bottom: 10px; + min-height: 80px; +} + +div.footer, div.footer a { + color: #888a85; +} + +div.footer .right { + text-align: right; +} + +div.footer .left { + text-transform: uppercase; +} + + +/* Styles copied from basic theme */ + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + clear: both; + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +/* -- viewcode extension ---------------------------------------------------- */ + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family:: "Verdana", Arial, sans-serif; +} + +div.viewcode-block:target { + margin: -1px -3px; + padding: 0 3px; + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} \ No newline at end of file diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 0000000..888716a --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,532 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + clear: both; + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #efefef; + width: 40%; + float: right; + -mox-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +p.sidebar-title { + font-weight: bold; + text-transform: uppercase; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} + diff --git a/docs/_static/default.css b/docs/_static/default.css new file mode 100644 index 0000000..21f3f50 --- /dev/null +++ b/docs/_static/default.css @@ -0,0 +1,256 @@ +/* + * default.css_t + * ~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- default theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + + + +/* -- hyperlink styles ------------------------------------------------------ */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:visited { + color: #355f7c; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + + + +/* -- body styles ----------------------------------------------------------- */ + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: justify; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: #eeffcc; + color: #333333; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +th { + background-color: #ede; +} + +.warning tt { + background: #efc2c2; +} + +.note tt { + background: #d6d6d6; +} + +.viewcode-back { + font-family: sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} \ No newline at end of file diff --git a/docs/_static/fonts/Museo_Slab_500.otf b/docs/_static/fonts/Museo_Slab_500.otf new file mode 100644 index 0000000..84ceaca Binary files /dev/null and b/docs/_static/fonts/Museo_Slab_500.otf differ diff --git a/docs/_static/fonts/Museo_Slab_500italic.otf b/docs/_static/fonts/Museo_Slab_500italic.otf new file mode 100644 index 0000000..a8c055f Binary files /dev/null and b/docs/_static/fonts/Museo_Slab_500italic.otf differ diff --git a/docs/_static/fonts/OFLGoudyStMTT-Italic.ttf b/docs/_static/fonts/OFLGoudyStMTT-Italic.ttf new file mode 100644 index 0000000..91956cb Binary files /dev/null and b/docs/_static/fonts/OFLGoudyStMTT-Italic.ttf differ diff --git a/docs/_static/fonts/OFLGoudyStMTT.ttf b/docs/_static/fonts/OFLGoudyStMTT.ttf new file mode 100644 index 0000000..dc900c2 Binary files /dev/null and b/docs/_static/fonts/OFLGoudyStMTT.ttf differ diff --git a/docs/_static/fonts/YanoneKaffeesatz-Bold.ttf b/docs/_static/fonts/YanoneKaffeesatz-Bold.ttf new file mode 100644 index 0000000..64e5d1c Binary files /dev/null and b/docs/_static/fonts/YanoneKaffeesatz-Bold.ttf differ diff --git a/docs/_static/fonts/YanoneKaffeesatz-Light.ttf b/docs/_static/fonts/YanoneKaffeesatz-Light.ttf new file mode 100644 index 0000000..15eccf5 Binary files /dev/null and b/docs/_static/fonts/YanoneKaffeesatz-Light.ttf differ diff --git a/docs/_static/fonts/YanoneKaffeesatz-Regular.ttf b/docs/_static/fonts/YanoneKaffeesatz-Regular.ttf new file mode 100644 index 0000000..25cee81 Binary files /dev/null and b/docs/_static/fonts/YanoneKaffeesatz-Regular.ttf differ diff --git a/docs/_static/fonts/YanoneKaffeesatz-Thin.ttf b/docs/_static/fonts/YanoneKaffeesatz-Thin.ttf new file mode 100644 index 0000000..6e26d9e Binary files /dev/null and b/docs/_static/fonts/YanoneKaffeesatz-Thin.ttf differ diff --git a/docs/_static/haiku.css b/docs/_static/haiku.css new file mode 100644 index 0000000..615ed47 --- /dev/null +++ b/docs/_static/haiku.css @@ -0,0 +1,406 @@ +/* + * haiku.css_t + * ~~~~~~~~~~~ + * + * Sphinx stylesheet -- haiku theme. + * + * Adapted from http://haiku-os.org/docs/Haiku-doc.css. + * Original copyright message: + * + * Copyright 2008-2009, Haiku. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Francois Revol + * Stephan Assmus + * Braden Ewing + * Humdinger + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + + +@font-face { + font-family: "Museo Slab"; + font-weight: normal; + font-style: normal; + src: local("Museo Slab"), + url("fonts/Museo_Slab_500.otf") format("opentype"); +} + +@font-face { + font-family: "Yanone Kaffeesatz"; + font-weight: bold; + font-style: normal; + src: local("Yanone Kaffeesatz"), + url("fonts/YanoneKaffeesatz-Bold.ttf") format("truetype"); +} + +@font-face { + font-family: "Yanone Kaffeesatz"; + font-weight: lighter; + font-style: normal; + src: local("Yanone Kaffeesatz"), + url("fonts/YanoneKaffeesatz-Regular.ttf") format("truetype"); +} + +html { + margin: 0px; + padding: 0px; + background: #FFF url(header.png) top left repeat-x; +} + +body { + line-height: 1.5; + margin: auto; + padding: 0px; + font-family: "Helvetica Neueu", Helvetica, sans-serif; + min-width: 59em; + max-width: 70em; + color: #444; +} + +div.footer { + padding: 8px; + font-size: 11px; + text-align: center; + letter-spacing: 0.5px; +} + +/* link colors and text decoration */ + +a:link { + font-weight: bold; + text-decoration: none; + color: #00ADEE; +} + +a:visited { + font-weight: bold; + text-decoration: none; + color: #00ADEE; +} + +a:hover, a:active { + text-decoration: underline; + color: #F46DBA; +} + +/* Some headers act as anchors, don't give them a hover effect */ + +h1 a:hover, a:active { + text-decoration: none; + color: #CFCFCF; +} + +h2 a:hover, a:active { + text-decoration: none; + color: #CFCFCF; +} + +h3 a:hover, a:active { + text-decoration: none; + color: #CFCFCF; +} + +h4 a:hover, a:active { + text-decoration: none; + color: #CFCFCF; +} + +a.headerlink { + color: #a7ce38; + padding-left: 5px; +} + +a.headerlink:hover { + color: #a7ce38; +} + +/* basic text elements */ + +div.content { + margin-top: 20px; + margin-left: 40px; + margin-right: 40px; + margin-bottom: 50px; + font-size: 0.9em; +} + +/* heading and navigation */ + +div.header { + position: relative; + margin-top: 125px; + height: 85px; + padding: 0 40px; + font-family: "Yanone Kaffeesatz"; +} +div.header h1 { + font-size: 2.6em; + font-weight: normal; + letter-spacing: 1px; + color: #CFCFCF; + border: 0; + margin: 0; + padding-top: 15px; + font-family: "Yanone Kaffeesatz"; + text-shadow: 1px 1px 1px rgba(175, 175, 175, .8); + font-variant: small-caps; +} +div.header h1 a { + font-weight: normal; + color: #00ADEE; +} +div.header h2 { + font-size: 1.3em; + font-weight: normal; + letter-spacing: 1px; + text-transform: uppercase; + color: #aaa; + border: 0; + margin-top: -3px; + padding: 0; + font-family: "Yanone Kaffeesatz"; +} + +div.header img.rightlogo { + float: right; +} + + +div.title { + font-size: 1.3em; + font-weight: bold; + color: #CFCFCF; + border-bottom: dotted thin #e0e0e0; + margin-bottom: 25px; +} +div.topnav { + position: relative; + z-index: 0; +} +div.topnav p { + margin-top: 0; + margin-left: 40px; + margin-right: 40px; + margin-bottom: 0px; + text-align: right; + font-size: 0.8em; +} +div.bottomnav { + background: #eeeeee; +} +div.bottomnav p { + margin-right: 40px; + text-align: right; + font-size: 0.8em; +} + +a.uplink { + font-weight: normal; +} + + +/* contents box */ + +table.index { + margin: 0px 0px 30px 30px; + padding: 1px; + border-width: 1px; + border-style: dotted; + border-color: #e0e0e0; +} +table.index tr.heading { + background-color: #e0e0e0; + text-align: center; + font-weight: bold; + font-size: 1.1em; +} +table.index tr.index { + background-color: #eeeeee; +} +table.index td { + padding: 5px 20px; +} + +table.index a:link, table.index a:visited { + font-weight: normal; + text-decoration: none; + color: #4A7389; +} +table.index a:hover, table.index a:active { + text-decoration: underline; + color: #ff4500; +} + + +/* Haiku User Guide styles and layout */ + +/* Rounded corner boxes */ +/* Common declarations */ +div.admonition { + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + border-style: dotted; + border-width: thin; + border-color: #dcdcdc; + padding: 10px 15px 10px 15px; + margin-bottom: 15px; + margin-top: 15px; +} +div.note { + padding: 10px 15px 10px 15px; + background-color: #e4ffde; + /*background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat;*/ + min-height: 42px; +} +div.warning { + padding: 10px 15px 10px 15px; + background-color: #fffbc6; + /*background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat;*/ + min-height: 42px; +} +div.seealso { + background: #e4ffde; +} + +/* More layout and styles */ +h1 { + font-size: 1.6em; + color: #aaa; + border-bottom: dotted thin #e0e0e0; + margin-top: 30px; + font-family: "Museo Slab"; + text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); +} + +h2 { + font-size: 1.5em; + font-weight: normal; + color: #aaa; + border-bottom: dotted thin #e0e0e0; + margin-top: 30px; + font-family: "Museo Slab"; + text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); +} + +h3 { + font-size: 1.4em; + font-weight: normal; + color: #aaa; + margin-top: 30px; + font-family: "Museo Slab"; + text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); +} + +h4 { + font-size: 1.3em; + font-weight: normal; + color: #CFCFCF; + margin-top: 30px; +} + +p { + text-align: justify; +} + +p.last { + margin-bottom: 0; +} + +ol { + padding-left: 20px; +} + +ul { + padding-left: 5px; + margin-top: 3px; +} + +li { + line-height: 1.3; +} + +div.content ul > li { + -moz-background-clip:border; + -moz-background-inline-policy:continuous; + -moz-background-origin:padding; + background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em; + list-style-image: none; + list-style-type: none; + padding: 0 0 0 1.666em; + margin-bottom: 3px; +} + +td { + vertical-align: top; +} + +tt { + background-color: #e2e2e2; + font-size: 1.0em; + font-family: monospace; +} + +pre { + font-size: 1.1em; + margin: 0 0 12px 0; + padding: 0.8em; + background-image: url(noise_dk.png); + background-color: #222; +} + +hr { + border-top: 1px solid #ccc; + border-bottom: 0; + border-right: 0; + border-left: 0; + margin-bottom: 10px; + margin-top: 20px; +} + +/* printer only pretty stuff */ +@media print { + .noprint { + display: none; + } + /* for acronyms we want their definitions inlined at print time */ + acronym[title]:after { + font-size: small; + content: " (" attr(title) ")"; + font-style: italic; + } + /* and not have mozilla dotted underline */ + acronym { + border: none; + } + div.topnav, div.bottomnav, div.header, table.index { + display: none; + } + div.content { + margin: 0px; + padding: 0px; + } + html { + background: #FFF; + } +} + +.viewcode-back { + font-family: "DejaVu Sans", Arial, Helvetica, sans-serif; +} + +div.viewcode-block:target { + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; + margin: -1px -12px; + padding: 0 12px; +} + diff --git a/docs/_static/header.png b/docs/_static/header.png new file mode 100644 index 0000000..2aaa53a Binary files /dev/null and b/docs/_static/header.png differ diff --git a/docs/_static/images/arch_layers.png b/docs/_static/images/arch_layers.png new file mode 100644 index 0000000..1ee183b Binary files /dev/null and b/docs/_static/images/arch_layers.png differ diff --git a/docs/_static/ir_black.css b/docs/_static/ir_black.css new file mode 100644 index 0000000..f04bc73 --- /dev/null +++ b/docs/_static/ir_black.css @@ -0,0 +1,70 @@ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #000000; color: #f6f3e8; } +.highlight .c { color: #7C7C7C; } /* Comment */ +.highlight .err { color: #f6f3e8; } /* Error */ +.highlight .g { color: #f6f3e8; } /* Generic */ +.highlight .k { color: #00ADEE; } /* Keyword */ +.highlight .l { color: #f6f3e8; } /* Literal */ +.highlight .n { color: #f6f3e8; } /* Name */ +.highlight .o { color: #f6f3e8; } /* Operator */ +.highlight .x { color: #f6f3e8; } /* Other */ +.highlight .p { color: #f6f3e8; } /* Punctuation */ +.highlight .cm { color: #7C7C7C; } /* Comment.Multiline */ +.highlight .cp { color: #96CBFE; } /* Comment.Preproc */ +.highlight .c1 { color: #7C7C7C; } /* Comment.Single */ +.highlight .cs { color: #7C7C7C; } /* Comment.Special */ +.highlight .gd { color: #f6f3e8; } /* Generic.Deleted */ +.highlight .ge { color: #f6f3e8; } /* Generic.Emph */ +.highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */ +.highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */ +.highlight .gi { color: #f6f3e8; } /* Generic.Inserted */ +.highlight .go { color: #070707; } /* Generic.Output */ +.highlight .gp { color: #f6f3e8; } /* Generic.Prompt */ +.highlight .gs { color: #f6f3e8; } /* Generic.Strong */ +.highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */ +.highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */ +.highlight .kc { color: #6699CC; } /* Keyword.Constant */ +.highlight .kd { color: #6699CC; } /* Keyword.Declaration */ +.highlight .kn { color: #6699CC; } /* Keyword.Namespace */ +.highlight .kp { color: #6699CC; } /* Keyword.Pseudo */ +.highlight .kr { color: #6699CC; } /* Keyword.Reserved */ +.highlight .kt { color: #FFFFB6; } /* Keyword.Type */ +.highlight .ld { color: #f6f3e8; } /* Literal.Date */ +.highlight .m { color: #FF73FD; } /* Literal.Number */ +.highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */ +.highlight .na { color: #f6f3e8; } /* Name.Attribute */ +.highlight .nb { color: #f6f3e8; } /* Name.Builtin */ +.highlight .nc { color: #f6f3e8; } /* Name.Class */ +.highlight .no { color: #99CC99; } /* Name.Constant */ +.highlight .nd { color: #f6f3e8; } /* Name.Decorator */ +.highlight .ni { color: #E18964; } /* Name.Entity */ +.highlight .ne { color: #f6f3e8; } /* Name.Exception */ +.highlight .nf { color: #F64DBA; } /* Name.Function */ +.highlight .nl { color: #f6f3e8; } /* Name.Label */ +.highlight .nn { color: #f6f3e8; } /* Name.Namespace */ +.highlight .nx { color: #f6f3e8; } /* Name.Other */ +.highlight .py { color: #f6f3e8; } /* Name.Property */ +.highlight .nt { color: #00ADEE; } /* Name.Tag */ +.highlight .nv { color: #C6C5FE; } /* Name.Variable */ +.highlight .ow { color: #ffffff; } /* Operator.Word */ +.highlight .w { color: #f6f3e8; } /* Text.Whitespace */ +.highlight .mf { color: #FF73FD; } /* Literal.Number.Float */ +.highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */ +.highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */ +.highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */ +.highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */ +.highlight .sc { color: #A8FF60; } /* Literal.String.Char */ +.highlight .sd { color: #A8FF60; } /* Literal.String.Doc */ +.highlight .s2 { color: #A8FF60; } /* Literal.String.Double */ +.highlight .se { color: #A8FF60; } /* Literal.String.Escape */ +.highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */ +.highlight .si { color: #A8FF60; } /* Literal.String.Interpol */ +.highlight .sx { color: #A8FF60; } /* Literal.String.Other */ +.highlight .sr { color: #A8FF60; } /* Literal.String.Regex */ +.highlight .s1 { color: #A8FF60; } /* Literal.String.Single */ +.highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */ +.highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */ +.highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */ +.highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */ +.highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */ diff --git a/docs/_static/nature.css b/docs/_static/nature.css new file mode 100644 index 0000000..52b328e --- /dev/null +++ b/docs/_static/nature.css @@ -0,0 +1,245 @@ +/* + * nature.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- nature theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + background-color: #111; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #C8D5E3; } +div.body h3 { font-size: 120%; background-color: #D8DEE3; } +div.body h4 { font-size: 110%; background-color: #D8DEE3; } +div.body h5 { font-size: 100%; background-color: #D8DEE3; } +div.body h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} \ No newline at end of file diff --git a/docs/_static/noise_dk.png b/docs/_static/noise_dk.png new file mode 100644 index 0000000..f0b4de4 Binary files /dev/null and b/docs/_static/noise_dk.png differ diff --git a/docs/_static/sphinxdoc.css b/docs/_static/sphinxdoc.css new file mode 100644 index 0000000..0a42807 --- /dev/null +++ b/docs/_static/sphinxdoc.css @@ -0,0 +1,339 @@ +/* + * sphinxdoc.css_t + * ~~~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- sphinxdoc theme. Originally created by + * Armin Ronacher for Werkzeug. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; + font-size: 14px; + letter-spacing: -0.01em; + line-height: 150%; + text-align: center; + background-color: #BFD1D4; + color: black; + padding: 0; + border: 1px solid #aaa; + + margin: 0px 80px 0px 80px; + min-width: 740px; +} + +div.document { + background-color: white; + text-align: left; + background-image: url(contents.png); + background-repeat: repeat-x; +} + +div.bodywrapper { + margin: 0 240px 0 0; + border-right: 1px solid #ccc; +} + +div.body { + margin: 0; + padding: 0.5em 20px 20px 20px; +} + +div.related { + font-size: 1em; +} + +div.related ul { + background-image: url(navigation.png); + height: 2em; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +div.related ul li { + margin: 0; + padding: 0; + height: 2em; + float: left; +} + +div.related ul li.right { + float: right; + margin-right: 5px; +} + +div.related ul li a { + margin: 0; + padding: 0 5px 0 5px; + line-height: 1.75em; + color: #EE9816; +} + +div.related ul li a:hover { + color: #3CA8E7; +} + +div.sphinxsidebarwrapper { + padding: 0; +} + +div.sphinxsidebar { + margin: 0; + padding: 0.5em 15px 15px 0; + width: 210px; + float: right; + font-size: 1em; + text-align: left; +} + +div.sphinxsidebar h3, div.sphinxsidebar h4 { + margin: 1em 0 0.5em 0; + font-size: 1em; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border: 1px solid #86989B; + background-color: #AFC1C4; +} + +div.sphinxsidebar h3 a { + color: white; +} + +div.sphinxsidebar ul { + padding-left: 1.5em; + margin-top: 7px; + padding: 0; + line-height: 130%; +} + +div.sphinxsidebar ul ul { + margin-left: 20px; +} + +div.footer { + background-color: #E3EFF1; + color: #86989B; + padding: 3px 8px 3px 0; + clear: both; + font-size: 0.8em; + text-align: right; +} + +div.footer a { + color: #86989B; + text-decoration: underline; +} + +/* -- body styles ----------------------------------------------------------- */ + +p { + margin: 0.8em 0 0.5em 0; +} + +a { + color: #CA7900; + text-decoration: none; +} + +a:hover { + color: #2491CF; +} + +div.body a { + text-decoration: underline; +} + +h1 { + margin: 0; + padding: 0.7em 0 0.3em 0; + font-size: 1.5em; + color: #11557C; +} + +h2 { + margin: 1.3em 0 0.2em 0; + font-size: 1.35em; + padding: 0; +} + +h3 { + margin: 1em 0 -0.3em 0; + font-size: 1.2em; +} + +div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { + color: black!important; +} + +h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { + display: none; + margin: 0 0 0 0.3em; + padding: 0 0.2em 0 0.2em; + color: #aaa!important; +} + +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, +h5:hover a.anchor, h6:hover a.anchor { + display: inline; +} + +h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, +h5 a.anchor:hover, h6 a.anchor:hover { + color: #777; + background-color: #eee; +} + +a.headerlink { + color: #c60f0f!important; + font-size: 1em; + margin-left: 6px; + padding: 0 4px 0 4px; + text-decoration: none!important; +} + +a.headerlink:hover { + background-color: #ccc; + color: white!important; +} + +cite, code, tt { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.01em; +} + +tt { + background-color: #f2f2f2; + border-bottom: 1px solid #ddd; + color: #333; +} + +tt.descname, tt.descclassname, tt.xref { + border: 0; +} + +hr { + border: 1px solid #abc; + margin: 2em; +} + +a tt { + border: 0; + color: #CA7900; +} + +a tt:hover { + color: #2491CF; +} + +pre { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.015em; + line-height: 120%; + padding: 0.5em; + border: 1px solid #ccc; + background-color: #f8f8f8; +} + +pre a { + color: inherit; + text-decoration: underline; +} + +td.linenos pre { + padding: 0.5em 0; +} + +div.quotebar { + background-color: #f8f8f8; + max-width: 250px; + float: right; + padding: 2px 7px; + border: 1px solid #ccc; +} + +div.topic { + background-color: #f8f8f8; +} + +table { + border-collapse: collapse; + margin: 0 -0.5em 0 -0.5em; +} + +table td, table th { + padding: 0.2em 0.5em 0.2em 0.5em; +} + +div.admonition, div.warning { + font-size: 0.9em; + margin: 1em 0 1em 0; + border: 1px solid #86989B; + background-color: #f7f7f7; + padding: 0; +} + +div.admonition p, div.warning p { + margin: 0.5em 1em 0.5em 1em; + padding: 0; +} + +div.admonition pre, div.warning pre { + margin: 0.4em 1em 0.4em 1em; +} + +div.admonition p.admonition-title, +div.warning p.admonition-title { + margin: 0; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border-bottom: 1px solid #86989B; + font-weight: bold; + background-color: #AFC1C4; +} + +div.warning { + border: 1px solid #940000; +} + +div.warning p.admonition-title { + background-color: #CF0000; + border-bottom-color: #940000; +} + +div.admonition ul, div.admonition ol, +div.warning ul, div.warning ol { + margin: 0.1em 0.5em 0.5em 3em; + padding: 0; +} + +div.versioninfo { + margin: 1em 0 0 0; + border: 1px solid #ccc; + background-color: #DDEAF0; + padding: 8px; + line-height: 1.3em; + font-size: 0.9em; +} + +.viewcode-back { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} diff --git a/docs/api/basexmpp.rst b/docs/api/basexmpp.rst new file mode 100644 index 0000000..fa96322 --- /dev/null +++ b/docs/api/basexmpp.rst @@ -0,0 +1,8 @@ +======== +BaseXMPP +======== + +.. module:: sleekxmpp.basexmpp + +.. autoclass:: BaseXMPP + :members: diff --git a/docs/api/clientxmpp.rst b/docs/api/clientxmpp.rst new file mode 100644 index 0000000..a6f32c4 --- /dev/null +++ b/docs/api/clientxmpp.rst @@ -0,0 +1,8 @@ +========== +ClientXMPP +========== + +.. module:: sleekxmpp.clientxmpp + +.. autoclass:: ClientXMPP + :members: diff --git a/docs/api/componentxmpp.rst b/docs/api/componentxmpp.rst new file mode 100644 index 0000000..989120c --- /dev/null +++ b/docs/api/componentxmpp.rst @@ -0,0 +1,8 @@ +============= +ComponentXMPP +============= + +.. module:: sleekxmpp.componentxmpp + +.. autoclass:: ComponentXMPP + :members: diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst new file mode 100644 index 0000000..7bc72ce --- /dev/null +++ b/docs/api/exceptions.rst @@ -0,0 +1,14 @@ +Exceptions +========== + +.. module:: sleekxmpp.exceptions + + +.. autoexception:: XMPPError + :members: + +.. autoexception:: IqError + :members: + +.. autoexception:: IqTimeout + :members: diff --git a/docs/api/xmlstream/filesocket.rst b/docs/api/xmlstream/filesocket.rst new file mode 100644 index 0000000..35f4401 --- /dev/null +++ b/docs/api/xmlstream/filesocket.rst @@ -0,0 +1,12 @@ +.. module:: sleekxmpp.xmlstream.filesocket + +.. _filesocket: + +Python 2.6 File Socket Shims +============================ + +.. autoclass:: FileSocket + :members: + +.. autoclass:: Socket26 + :members: diff --git a/docs/api/xmlstream/handler.rst b/docs/api/xmlstream/handler.rst new file mode 100644 index 0000000..33c0bf4 --- /dev/null +++ b/docs/api/xmlstream/handler.rst @@ -0,0 +1,24 @@ +Stanza Handlers +=============== + +The Basic Handler +----------------- +.. module:: sleekxmpp.xmlstream.handler.base + +.. autoclass:: BaseHandler + :members: + +Callback +-------- +.. module:: sleekxmpp.xmlstream.handler.callback + +.. autoclass:: Callback + :members: + + +Waiter +------ +.. module:: sleekxmpp.xmlstream.handler.waiter + +.. autoclass:: Waiter + :members: diff --git a/docs/api/xmlstream/jid.rst b/docs/api/xmlstream/jid.rst new file mode 100644 index 0000000..22a2db4 --- /dev/null +++ b/docs/api/xmlstream/jid.rst @@ -0,0 +1,7 @@ +Jabber IDs (JID) +================= + +.. module:: sleekxmpp.xmlstream.jid + +.. autoclass:: JID + :members: diff --git a/docs/api/xmlstream/matcher.rst b/docs/api/xmlstream/matcher.rst new file mode 100644 index 0000000..df3591b --- /dev/null +++ b/docs/api/xmlstream/matcher.rst @@ -0,0 +1,41 @@ +Stanza Matchers +=============== + +The Basic Matcher +----------------- +.. module:: sleekxmpp.xmlstream.matcher.base + +.. autoclass:: MatcherBase + :members: + + +ID Matching +----------- +.. module:: sleekxmpp.xmlstream.matcher.id + +.. autoclass:: MatcherId + :members: + + +Stanza Path Matching +-------------------- +.. module:: sleekxmpp.xmlstream.matcher.stanzapath + +.. autoclass:: StanzaPath + :members: + + +XPath +----- +.. module:: sleekxmpp.xmlstream.matcher.xpath + +.. autoclass:: MatchXPath + :members: + + +XMLMask +------- +.. module:: sleekxmpp.xmlstream.matcher.xmlmask + +.. autoclass:: MatchXMLMask + :members: diff --git a/docs/api/xmlstream/scheduler.rst b/docs/api/xmlstream/scheduler.rst new file mode 100644 index 0000000..ff91701 --- /dev/null +++ b/docs/api/xmlstream/scheduler.rst @@ -0,0 +1,11 @@ +========= +Scheduler +========= + +.. module:: sleekxmpp.xmlstream.scheduler + +.. autoclass:: Task + :members: + +.. autoclass:: Scheduler + :members: diff --git a/docs/api/xmlstream/stanzabase.rst b/docs/api/xmlstream/stanzabase.rst new file mode 100644 index 0000000..f575299 --- /dev/null +++ b/docs/api/xmlstream/stanzabase.rst @@ -0,0 +1,123 @@ +.. _stanzabase: + +============== +Stanza Objects +============== + +.. module:: sleekxmpp.xmlstream.stanzabase + +The :mod:`~sleekmxpp.xmlstream.stanzabase` module provides a wrapper for the +standard :mod:`~xml.etree.ElementTree` module that makes working with XML +less painful. Instead of having to manually move up and down an element +tree and insert subelements and attributes, you can interact with an object +that behaves like a normal dictionary or JSON object, which silently maps +keys to XML attributes and elements behind the scenes. + +Overview +-------- + +The usefulness of this layer grows as the XML you have to work with +becomes nested. The base unit here, :class:`ElementBase`, can map to a +single XML element, or several depending on how advanced of a mapping +is desired from interface keys to XML structures. For example, a single +:class:`ElementBase` derived class could easily describe: + +.. code-block:: xml + + + Hi! + + Custom item 1 + Custom item 2 + Custom item 3 + + + +If that chunk of XML were put in the :class:`ElementBase` instance +``msg``, we could extract the data from the XML using:: + + >>> msg['extra'] + ['Custom item 1', 'Custom item 2', 'Custom item 3'] + +Provided we set up the handler for the ``'extra'`` interface to load the +```` element content into a list. + +The key concept is that given an XML structure that will be repeatedly +used, we can define a set of :term:`interfaces` which when we read from, +write to, or delete, will automatically manipulate the underlying XML +as needed. In addition, some of these interfaces may in turn reference +child objects which expose interfaces for particularly complex child +elements of the original XML chunk. + +.. seealso:: + :ref:`create-stanza-interfaces`. + +Because the :mod:`~sleekxmpp.xmlstream.stanzabase` module was developed +as part of an `XMPP `_ library, these chunks of XML are +referred to as :term:`stanzas `, and in SleekXMPP we refer to a +subclass of :class:`ElementBase` which defines the interfaces needed for +interacting with a given :term:`stanza` a :term:`stanza object`. + +To make dealing with more complicated and nested :term:`stanzas ` +or XML chunks easier, :term:`stanza objects ` can be +composed in two ways: as iterable child objects or as plugins. Iterable +child stanzas, or :term:`substanzas`, are accessible through a special +``'substanzas'`` interface. This option is useful for stanzas which +may contain more than one of the same kind of element. When there is +only one child element, the plugin method is more useful. For plugins, +a parent stanza object delegates one of its XML child elements to the +plugin stanza object. Here is an example: + +.. code-block:: xml + + + + + + + +We can can arrange this stanza into two objects: an outer, wrapper object for +dealing with the ```` element and its attributes, and a plugin object to +control the ```` payload element. If we give the plugin object the +name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then +we can access the plugin as so:: + + >>> iq['disco_info'] + ' + + ' + +We can then drill down through the plugin object's interfaces as desired:: + + >>> iq['disco_info']['identities'] + [('client', 'bot', 'SleekXMPP Bot')] + +Plugins may also add new interfaces to the parent stanza object as if they +had been defined by the parent directly, and can also override the behaviour +of an interface defined by the parent. + +.. seealso:: + + - :ref:`create-stanza-plugins` + - :ref:`create-extension-plugins` + - :ref:`override-parent-interfaces` + + +Registering Stanza Plugins +-------------------------- + +.. autofunction:: register_stanza_plugin + +ElementBase +----------- + +.. autoclass:: ElementBase + :members: + :private-members: + :special-members: + +StanzaBase +---------- + +.. autoclass:: StanzaBase + :members: diff --git a/docs/api/xmlstream/tostring.rst b/docs/api/xmlstream/tostring.rst new file mode 100644 index 0000000..82a8c2a --- /dev/null +++ b/docs/api/xmlstream/tostring.rst @@ -0,0 +1,46 @@ +.. module:: sleekxmpp.xmlstream.tostring + +.. _tostring: + +XML Serialization +================= + +Since the XML layer of SleekXMPP is based on :mod:`~xml.etree.ElementTree`, +why not just use the built-in :func:`~xml.etree.ElementTree.tostring` +method? The answer is that using that method produces ugly results when +using namespaces. The :func:`tostring()` method used here intelligently +hides namespaces when able and does not introduce excessive namespace +prefixes:: + + >>> from sleekxmpp.xmlstream.tostring import tostring + >>> from xml.etree import cElementTree as ET + >>> xml = ET.fromstring('') + >>> ET.tostring(xml) + '' + >>> tostring(xml) + '' + +As a side effect of this namespace hiding, using :func:`tostring()` may +produce unexpected results depending on how the :func:`tostring()` method +is invoked. For example, when sending XML on the wire, the main XMPP +stanzas with their namespace of ``jabber:client`` will not include the +namespace because that is already declared by the stream header. But, if +you create a :class:`~sleekxmpp.stanza.message.Message` instance and dump +it to the terminal, the ``jabber:client`` namespace will appear. + +.. autofunction:: tostring + +Escaping Special Characters +--------------------------- + +In order to prevent errors when sending arbitrary text as the textual +content of an XML element, certain characters must be escaped. These +are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping +mechanism is to replace those characters with their equivalent escape +entities: ``&``, ``<``, ``>``, ``'``, and ``"``. + +In the future, the use of CDATA sections may be allowed to reduce the +size of escaped text or for when other XMPP processing agents do not +undertand these entities. + +.. autofunction:: xml_escape diff --git a/docs/api/xmlstream/xmlstream.rst b/docs/api/xmlstream/xmlstream.rst new file mode 100644 index 0000000..90a7a6a --- /dev/null +++ b/docs/api/xmlstream/xmlstream.rst @@ -0,0 +1,10 @@ +========== +XML Stream +========== + +.. module:: sleekxmpp.xmlstream.xmlstream + +.. autoexception:: RestartStream + +.. autoclass:: XMLStream + :members: diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..a2e0a27 --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,177 @@ +.. index:: XMLStream, BaseXMPP, ClientXMPP, ComponentXMPP + +SleekXMPP Architecture +====================== + +The core of SleekXMPP is contained in four classes: ``XMLStream``, +``BaseXMPP``, ``ClientXMPP``, and ``ComponentXMPP``. Along side this +stack is a library for working with XML objects that eliminates most +of the tedium of creating/manipulating XML. + +.. image:: _static/images/arch_layers.png + :height: 300px + :align: center + + +.. index:: XMLStream + +The Foundation: XMLStream +------------------------- +:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` is a mostly XMPP-agnostic +class whose purpose is to read and write from a bi-directional XML stream. +It also allows for callback functions to execute when XML matching given +patterns is received; these callbacks are also referred to as :term:`stream +handlers `. The class also provides a basic eventing system +which can be triggered either manually or on a timed schedule. + +The Main Threads +~~~~~~~~~~~~~~~~ +:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` instances run using at +least three background threads: the send thread, the read thread, and the +scheduler thread. The send thread is in charge of monitoring the send queue +and writing text to the outgoing XML stream. The read thread pulls text off +of the incoming XML stream and stores the results in an event queue. The +scheduler thread is used to emit events after a given period of time. + +Additionally, the main event processing loop may be executed in its +own thread if SleekXMPP is being used in the background for another +application. + +Short-lived threads may also be spawned as requested for threaded +:term:`event handlers `. + +How XML Text is Turned into Action +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To demonstrate the flow of information, let's consider what happens +when this bit of XML is received (with an assumed namespace of +``jabber:client``): + +.. code-block:: xml + + + Hej! + + + +1. **Convert XML strings into objects.** + + Incoming text is parsed and converted into XML objects (using + ElementTree) which are then wrapped into what are referred to as + :term:`Stanza objects `. The appropriate class for the + new object is determined using a map of namespaced element names to + classes. + + Our incoming XML is thus turned into a :class:`~sleekxmpp.stanza.Message` + :term:`stanza object` because the namespaced element name + ``{jabber:client}message`` is associated with the class + :class:`~sleekxmpp.stanza.Message`. + +2. **Match stanza objects to callbacks.** + + These objects are then compared against the stored patterns associated + with the registered callback handlers. For each match, a copy of the + :term:`stanza object` is paired with a reference to the handler and + placed into the event queue. + + Our :class:`~sleekxmpp.stanza.Message` object is thus paired with the message stanza handler + :meth:`BaseXMPP._handle_message` to create the tuple:: + + ('stanza', stanza_obj, handler) + +3. **Process the event queue.** + + The event queue is the heart of SleekXMPP. Nearly every action that + takes place is first inserted into this queue, whether that be received + stanzas, custom events, or scheduled events. + + When the stanza is pulled out of the event queue with an associated + callback, the callback function is executed with the stanza as its only + parameter. + + .. warning:: + The callback, aka :term:`stream handler`, is executed in the main event + processing thread. If the handler blocks, event processing will also + block. + +4. **Raise Custom Events** + + Since a :term:`stream handler` shouldn't block, if extensive processing + for a stanza is required (such as needing to send and receive an + :class:`~sleekxmpp.stanza.Iq` stanza), then custom events must be used. + These events are not explicitly tied to the incoming XML stream and may + be raised at any time. Importantly, these events may be handled in their + own thread. + + When the event is raised, a copy of the stanza is created for each + handler registered for the event. In contrast to :term:`stream handlers + `, these functions are referred to as :term:`event + handlers `. Each stanza/handler pair is then put into the + event queue. + + .. note:: + It is possible to skip the event queue and process an event immediately + by using ``direct=True`` when raising the event. + + The code for :meth:`BaseXMPP._handle_message` follows this pattern, and + raises a ``'message'`` event:: + + self.event('message', msg) + + The event call then places the message object back into the event queue + paired with an :term:`event handler`:: + + ('event', 'message', msg_copy1, custom_event_handler_1) + ('event', 'message', msg_copy2, custom_evetn_handler_2) + +5. **Process Custom Events** + + The stanza and :term:`event handler` are then pulled from the event + queue, and the handler is executed, passing the stanza as its only + argument. If the handler was registered as threaded, then a new thread + will be spawned for it. + + .. note:: + Events may be raised without needing :term:`stanza objects `. + For example, you could use ``self.event('custom', {'a': 'b'})``. + You don't even need any arguments: ``self.event('no_parameters')``. + However, every event handler MUST accept at least one argument. + + Finally, after a long trek, our message is handed off to the user's + custom handler in order to do awesome stuff:: + + msg.reply() + msg['body'] = "Hey! This is awesome!" + msg.send() + + +.. index:: BaseXMPP, XMLStream + +Raising XMPP Awareness: BaseXMPP +-------------------------------- +While :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` attempts to shy away +from anything too XMPP specific, :class:`~sleekxmpp.basexmpp.BaseXMPP`'s +sole purpose is to provide foundational support for sending and receiving +XMPP stanzas. This support includes registering the basic message, +presence, and iq stanzas, methods for creating and sending stanzas, and +default handlers for incoming messages and keeping track of presence +notifications. + +The plugin system for adding new XEP support is also maintained by +:class:`~sleekxmpp.basexmpp.BaseXMPP`. + +.. index:: ClientXMPP, BaseXMPP + +ClientXMPP +---------- +:class:`~sleekxmpp.clientxmpp.ClientXMPP` extends +:class:`~sleekxmpp.clientxmpp.BaseXMPP` with additional logic for connecting +to an XMPP server by performing DNS lookups. It also adds support for stream +features such as STARTTLS and SASL. + +.. index:: ComponentXMPP, BaseXMPP + +ComponentXMPP +------------- +:class:`~sleekxmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of +:class:`~sleekxmpp.basexmpp.BaseXMPP` that implements the component handshake +protocol. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..dd83f24 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# +# SleekXMPP documentation build configuration file, created by +# sphinx-quickstart on Tue Aug 9 22:27:06 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.insert(0, os.path.abspath('..')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# 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.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] + +# 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-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'SleekXMPP' +copyright = u'2011, Nathan Fritz, Lance Stout' + +# 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.0RC3' + +# 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 patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_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 = 'tango' + +# 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. See the documentation for +# a list of builtin themes. +html_theme = 'nature' + +# 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 = {'headingcolor': '#CFCFCF', 'linkcolor': '#4A7389'} + +# 00ADEE + +# 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 +# " v documentation". +html_title = 'SleekXMPP' + +# A shorter title for the navigation bar. Default is the same as html_title. +html_short_title = '%s Documentation' % release + +# 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'SleekXMPPdoc' + + +# -- 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', 'SleekXMPP.tex', u'SleekXMPP Documentation', + u'Nathan Fritz, Lance Stout', '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 + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = 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_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'sleekxmpp', u'SleekXMPP Documentation', + [u'Nathan Fritz, Lance Stout'], 1) +] + +intersphinx_mapping = {'python': ('http://docs.python.org/3.2', 'python-objects.inv')} diff --git a/docs/create_plugin.rst b/docs/create_plugin.rst new file mode 100644 index 0000000..12efa84 --- /dev/null +++ b/docs/create_plugin.rst @@ -0,0 +1,679 @@ +.. _create-plugin: + +Creating a SleekXMPP Plugin +=========================== + +One of the goals of SleekXMPP is to provide support for every draft or final +XMPP extension (`XEP `_). To do this, SleekXMPP has a +plugin mechanism for adding the functionalities required by each XEP. But even +though plugins were made to quickly implement and prototype the official XMPP +extensions, there is no reason you can't create your own plugin to implement +your own custom XMPP-based protocol. + +This guide will help walk you through the steps to +implement a rudimentary version of `XEP-0077 In-band +Registration `_. In-band registration +was implemented in example 14-6 (page 223) of `XMPP: The Definitive +Guide `_ because there was no SleekXMPP +plugin for XEP-0077 at the time of writing. We will partially fix that issue +here by turning the example implementation from *XMPP: The Definitive Guide* +into a plugin. Again, note that this will not a complete implementation, and a +different, more robust, official plugin for XEP-0077 may be added to SleekXMPP +in the future. + +.. note:: + + The example plugin created in this guide is for the server side of the + registration process only. It will **NOT** be able to register new accounts + on an XMPP server. + +First Steps +----------- +Every plugin inherits from the class :mod:`base_plugin `, +and must include a ``plugin_init`` method. While the +plugins distributed with SleekXMPP must be placed in the plugins directory +``sleekxmpp/plugins`` to be loaded, custom plugins may be loaded from any +module. To do so, use the following form when registering the plugin: + +.. code-block:: python + + self.register_plugin('myplugin', module=mod_containing_my_plugin) + +The plugin name must be the same as the plugin's class name. + +Now, we can open our favorite text editors and create ``xep_0077.py`` in +``SleekXMPP/sleekxmpp/plugins``. We want to do some basic house-keeping and +declare the name and description of the XEP we are implementing. If you +are creating your own custom plugin, you don't need to include the ``xep`` +attribute. + +.. code-block:: python + + """ + Creating a SleekXMPP Plugin + + This is a minimal implementation of XEP-0077 to serve + as a tutorial for creating SleekXMPP plugins. + """ + + from sleekxmpp.plugins.base import base_plugin + + class xep_0077(base_plugin): + """ + XEP-0077 In-Band Registration + """ + + def plugin_init(self): + self.description = "In-Band Registration" + self.xep = "0077" + +Now that we have a basic plugin, we need to edit +``sleekxmpp/plugins/__init__.py`` to include our new plugin by adding +``'xep_0077'`` to the ``__all__`` declaration. + +Interacting with Other Plugins +------------------------------ + +In-band registration is a feature that should be advertised through `Service +Discovery `_. To do that, we tell the +``xep_0030`` plugin to add the ``"jabber:iq:register"`` feature. We put this +call in a method named ``post_init`` which will be called once the plugin has +been loaded; by doing so we advertise that we can do registrations only after we +finish activating the plugin. + +The ``post_init`` method needs to call ``base_plugin.post_init(self)`` +which will mark that ``post_init`` has been called for the plugin. Once the +SleekXMPP object begins processing, ``post_init`` will be called on any plugins +that have not already run ``post_init``. This allows you to register plugins and +their dependencies without needing to worry about the order in which you do so. + +**Note:** by adding this call we have introduced a dependency on the XEP-0030 +plugin. Be sure to register ``'xep_0030'`` as well as ``'xep_0077'``. SleekXMPP +does not automatically load plugin dependencies for you. + +.. code-block:: python + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature("jabber:iq:register") + +Creating Custom Stanza Objects +------------------------------ + +Now, the IQ stanzas needed to implement our version of XEP-0077 are not very +complex, and we could just interact with the XML objects directly just like +in the *XMPP: The Definitive Guide* example. However, creating custom stanza +objects is good practice. + +We will create a new ``Registration`` stanza. Following the *XMPP: The +Definitive Guide* example, we will add support for a username and password +field. We also need two flags: ``registered`` and ``remove``. The ``registered`` +flag is sent when an already registered user attempts to register, along with +their registration data. The ``remove`` flag is a request to unregister a user's +account. + +Adding additional `fields specified in +XEP-0077 `_ +will not be difficult and is left as an exercise for the reader. + +Our ``Registration`` class needs to start with a few descriptions of its +behaviour: + +* ``namespace`` + The namespace our stanza object lives in. In this case, + ``"jabber:iq:register"``. + +* ``name`` + The name of the root XML element. In this case, the ``query`` element. + +* ``plugin_attrib`` + The name to access this type of stanza. In particular, given a + registration stanza, the ``Registration`` object can be found using: + ``iq_object['register']``. + +* ``interfaces`` + A list of dictionary-like keys that can be used with the stanza object. + When using ``"key"``, if there exists a method of the form ``getKey``, + ``setKey``, or``delKey`` (depending on context) then the result of calling + that method will be returned. Otherwise, the value of the attribute ``key`` + of the main stanza element is returned if one exists. + + **Note:** The accessor methods currently use title case, and not camel case. + Thus if you need to access an item named ``"methodName"`` you will need to + use ``getMethodname``. This naming convention might change to full camel + case in a future version of SleekXMPP. + +* ``sub_interfaces`` + A subset of ``interfaces``, but these keys map to the text of any + subelements that are direct children of the main stanza element. Thus, + referencing ``iq_object['register']['username']`` will either execute + ``getUsername`` or return the value in the ``username`` element of the + query. + + If you need to access an element, say ``elem``, that is not a direct child + of the main stanza element, you will need to add ``getElem``, ``setElem``, + and ``delElem``. See the note above about naming conventions. + +.. code-block:: python + + from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin + from sleekxmpp import Iq + + class Registration(ElementBase): + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('username', 'password', 'registered', 'remove')) + sub_interfaces = interfaces + + def getRegistered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def getRemove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def setRegistered(self, registered): + if registered: + self.addField('registered') + else: + del self['registered'] + + def setRemove(self, remove): + if remove: + self.addField('remove') + else: + del self['remove'] + + def addField(self, name): + itemXML = ET.Element('{%s}%s' % (self.namespace, name)) + self.xml.append(itemXML) + +Setting a ``sub_interface`` attribute to ``""`` will remove that subelement. +Since we want to include empty registration fields in our form, we need the +``addField`` method to add the empty elements. + +Since the ``registered`` and ``remove`` elements are just flags, we need to add +custom logic to enforce the binary behavior. + +Extracting Stanzas from the XML Stream +-------------------------------------- + +Now that we have a custom stanza object, we need to be able to detect when we +receive one. To do this, we register a stream handler that will pattern match +stanzas off of the XML stream against our stanza object's element name and +namespace. To do so, we need to create a ``Callback`` object which contains +an XML fragment that can identify our stanza type. We can add this handler +registration to our ``plugin_init`` method. + +Also, we need to associate our ``Registration`` class with IQ stanzas; +that requires the use of the ``register_stanza_plugin`` function (in +``sleekxmpp.xmlstream.stanzabase``) which takes the class of a parent stanza +type followed by the substanza type. In our case, the parent stanza is an IQ +stanza, and the substanza is our registration query. + +The ``__handleRegistration`` method referenced in the callback will be our +handler function to process registration requests. + +.. code-block:: python + + def plugin_init(self): + self.description = "In-Band Registration" + self.xep = "0077" + + self.xmpp.registerHandler( + Callback('In-Band Registration', + MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), + self.__handleRegistration)) + register_stanza_plugin(Iq, Registration) + +Handling Incoming Stanzas and Triggering Events +----------------------------------------------- +There are six situations that we need to handle to finish our implementation of +XEP-0077. + +**Registration Form Request from a New User:** + + .. code-block:: xml + + + + + + + + +**Registration Form Request from an Existing User:** + + .. code-block:: xml + + + + + Foo + hunter2 + + + +**Unregister Account:** + + .. code-block:: xml + + + + + +**Incomplete Registration:** + + .. code-block:: xml + + + + Foo + + + + + + +**Conflicting Registrations:** + + .. code-block:: xml + + + + Foo + hunter2 + + + + + + +**Successful Registration:** + + .. code-block:: xml + + + + + +Cases 1 and 2: Registration Requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Responding to registration requests depends on if the requesting user already +has an account. If there is an account, the response should include the +``registered`` flag and the user's current registration information. Otherwise, +we just send the fields for our registration form. + +We will handle both cases by creating a ``sendRegistrationForm`` method that +will create either an empty of full form depending on if we provide it with +user data. Since we need to know which form fields to include (especially if we +add support for the other fields specified in XEP-0077), we will also create a +method ``setForm`` which will take the names of the fields we wish to include. + +.. code-block:: python + + def plugin_init(self): + self.description = "In-Band Registration" + self.xep = "0077" + self.form_fields = ('username', 'password') + ... remainder of plugin_init + + ... + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + userData = self.backend[iq['from'].bare] + self.sendRegistrationForm(iq, userData) + + def setForm(self, *fields): + self.form_fields = fields + + def sendRegistrationForm(self, iq, userData=None): + reg = iq['register'] + if userData is None: + userData = {} + else: + reg['registered'] = True + + for field in self.form_fields: + data = userData.get(field, '') + if data: + # Add field with existing data + reg[field] = data + else: + # Add a blank field + reg.addField(field) + + iq.reply().setPayload(reg.xml) + iq.send() + +Note how we are able to access our ``Registration`` stanza object with +``iq['register']``. + +A User Backend +++++++++++++++ +You might have noticed the reference to ``self.backend``, which is an object +that abstracts away storing and retrieving user information. Since it is not +much more than a dictionary, we will leave the implementation details to the +final, full source code example. + +Case 3: Unregister an Account +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The next simplest case to consider is responding to a request to remove +an account. If we receive a ``remove`` flag, we instruct the backend to +remove the user's account. Since your application may need to know about +when users are registered or unregistered, we trigger an event using +``self.xmpp.event('unregister_user', iq)``. See the component examples below for +how to respond to that event. + +.. code-block:: python + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + userData = self.backend[iq['from'].bare] + self.sendRegistrationForm(iq, userData) + elif iq['type'] == 'set': + # Remove an account + if iq['register']['remove']: + self.backend.unregister(iq['from'].bare) + self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + +Case 4: Incomplete Registration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +For the next case we need to check the user's registration to ensure it has all +of the fields we wanted. The simple option that we will use is to loop over the +field names and check each one; however, this means that all fields we send to +the user are required. Adding optional fields is left to the reader. + +Since we have received an incomplete form, we need to send an error message back +to the user. We have to send a few different types of errors, so we will also +create a ``_sendError`` method that will add the appropriate ``error`` element +to the IQ reply. + +.. code-block:: python + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + userData = self.backend[iq['from'].bare] + self.sendRegistrationForm(iq, userData) + elif iq['type'] == 'set': + if iq['register']['remove']: + # Remove an account + self.backend.unregister(iq['from'].bare) + self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + + for field in self.form_fields: + if not iq['register'][field]: + # Incomplete Registration + self._sendError(iq, '406', 'modify', 'not-acceptable' + "Please fill in all fields.") + return + + ... + + def _sendError(self, iq, code, error_type, name, text=''): + iq.reply().setPayload(iq['register'].xml) + iq.error() + iq['error']['code'] = code + iq['error']['type'] = error_type + iq['error']['condition'] = name + iq['error']['text'] = text + iq.send() + +Cases 5 and 6: Conflicting and Successful Registration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +We are down to the final decision on if we have a successful registration. We +send the user's data to the backend with the ``self.backend.register`` method. +If it returns ``True``, then registration has been successful. Otherwise, +there has been a conflict with usernames and registration has failed. Like +with unregistering an account, we trigger an event indicating that a user has +been registered by using ``self.xmpp.event('registered_user', iq)``. See the +component examples below for how to respond to this event. + +.. code-block:: python + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + userData = self.backend[iq['from'].bare] + self.sendRegistrationForm(iq, userData) + elif iq['type'] == 'set': + if iq['register']['remove']: + # Remove an account + self.backend.unregister(iq['from'].bare) + self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + + for field in self.form_fields: + if not iq['register'][field]: + # Incomplete Registration + self._sendError(iq, '406', 'modify', 'not-acceptable', + "Please fill in all fields.") + return + + if self.backend.register(iq['from'].bare, iq['register']): + # Successful registration + self.xmpp.event('registered_user', iq) + iq.reply().setPayload(iq['register'].xml) + iq.send() + else: + # Conflicting registration + self._sendError(iq, '409', 'cancel', 'conflict', + "That username is already taken.") + +Example Component Using the XEP-0077 Plugin +------------------------------------------- +Alright, the moment we've been working towards - actually using our plugin to +simplify our other applications. Here is a basic component that simply manages +user registrations and sends the user a welcoming message when they register, +and a farewell message when they delete their account. + +Note that we have to register the ``'xep_0030'`` plugin first, +and that we specified the form fields we wish to use with +``self.xmpp.plugin['xep_0077'].setForm('username', 'password')``. + +.. code-block:: python + + import sleekxmpp.componentxmpp + + class Example(sleekxmpp.componentxmpp.ComponentXMPP): + + def __init__(self, jid, password): + sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 8888) + + self.registerPlugin('xep_0030') + self.registerPlugin('xep_0077') + self.plugin['xep_0077'].setForm('username', 'password') + + self.add_event_handler("registered_user", self.reg) + self.add_event_handler("unregistered_user", self.unreg) + + def reg(self, iq): + msg = "Welcome! %s" % iq['register']['username'] + self.sendMessage(iq['from'], msg, mfrom=self.fulljid) + + def unreg(self, iq): + msg = "Bye! %s" % iq['register']['username'] + self.sendMessage(iq['from'], msg, mfrom=self.fulljid) + +**Congratulations!** We now have a basic, functioning implementation of +XEP-0077. + +Complete Source Code for XEP-0077 Plugin +---------------------------------------- +Here is a copy of a more complete implementation of the plugin we created, but +with some additional registration fields implemented. + +.. code-block:: python + + """ + Creating a SleekXMPP Plugin + + This is a minimal implementation of XEP-0077 to serve + as a tutorial for creating SleekXMPP plugins. + """ + + from sleekxmpp.plugins.base import base_plugin + from sleekxmpp.xmlstream.handler.callback import Callback + from sleekxmpp.xmlstream.matcher.xpath import MatchXPath + from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin + from sleekxmpp import Iq + import copy + + + class Registration(ElementBase): + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('username', 'password', 'email', 'nick', 'name', + 'first', 'last', 'address', 'city', 'state', 'zip', + 'phone', 'url', 'date', 'misc', 'text', 'key', + 'registered', 'remove', 'instructions')) + sub_interfaces = interfaces + + def getRegistered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def getRemove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def setRegistered(self, registered): + if registered: + self.addField('registered') + else: + del self['registered'] + + def setRemove(self, remove): + if remove: + self.addField('remove') + else: + del self['remove'] + + def addField(self, name): + itemXML = ET.Element('{%s}%s' % (self.namespace, name)) + self.xml.append(itemXML) + + + class UserStore(object): + def __init__(self): + self.users = {} + + def __getitem__(self, jid): + return self.users.get(jid, None) + + def register(self, jid, registration): + username = registration['username'] + + def filter_usernames(user): + return user != jid and self.users[user]['username'] == username + + conflicts = filter(filter_usernames, self.users.keys()) + if conflicts: + return False + + self.users[jid] = registration + return True + + def unregister(self, jid): + del self.users[jid] + + class xep_0077(base_plugin): + """ + XEP-0077 In-Band Registration + """ + + def plugin_init(self): + self.description = "In-Band Registration" + self.xep = "0077" + self.form_fields = ('username', 'password') + self.form_instructions = "" + self.backend = UserStore() + + self.xmpp.registerHandler( + Callback('In-Band Registration', + MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), + self.__handleRegistration)) + register_stanza_plugin(Iq, Registration) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature("jabber:iq:register") + + def __handleRegistration(self, iq): + if iq['type'] == 'get': + # Registration form requested + userData = self.backend[iq['from'].bare] + self.sendRegistrationForm(iq, userData) + elif iq['type'] == 'set': + if iq['register']['remove']: + # Remove an account + self.backend.unregister(iq['from'].bare) + self.xmpp.event('unregistered_user', iq) + iq.reply().send() + return + + for field in self.form_fields: + if not iq['register'][field]: + # Incomplete Registration + self._sendError(iq, '406', 'modify', 'not-acceptable', + "Please fill in all fields.") + return + + if self.backend.register(iq['from'].bare, iq['register']): + # Successful registration + self.xmpp.event('registered_user', iq) + iq.reply().setPayload(iq['register'].xml) + iq.send() + else: + # Conflicting registration + self._sendError(iq, '409', 'cancel', 'conflict', + "That username is already taken.") + + def setForm(self, *fields): + self.form_fields = fields + + def setInstructions(self, instructions): + self.form_instructions = instructions + + def sendRegistrationForm(self, iq, userData=None): + reg = iq['register'] + if userData is None: + userData = {} + else: + reg['registered'] = True + + if self.form_instructions: + reg['instructions'] = self.form_instructions + + for field in self.form_fields: + data = userData.get(field, '') + if data: + # Add field with existing data + reg[field] = data + else: + # Add a blank field + reg.addField(field) + + iq.reply().setPayload(reg.xml) + iq.send() + + def _sendError(self, iq, code, error_type, name, text=''): + iq.reply().setPayload(iq['register'].xml) + iq.error() + iq['error']['code'] = code + iq['error']['type'] = error_type + iq['error']['condition'] = name + iq['error']['text'] = text + iq.send() diff --git a/docs/event_index.rst b/docs/event_index.rst new file mode 100644 index 0000000..2c5dfd3 --- /dev/null +++ b/docs/event_index.rst @@ -0,0 +1,271 @@ +Event Index +=========== + +.. glossary:: + :sorted: + + connected + - **Data:** ``{}`` + - **Source:** :py:class:`~sleekxmpp.clientxmpp.ClientXMPP` + + Signal that a connection has been made with the XMPP server, but a session + has not yet been established. + + changed_status + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + Triggered when a presence stanza is received from a JID with a show type + different than the last presence stanza from the same JID. + + changed_subscription + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + Triggered whenever a presence stanza with a type of ``subscribe``, + ``subscribed``, ``unsubscribe``, or ``unsubscribed`` is received. + + Note that if the values ``xmpp.auto_authorize`` and ``xmpp.auto_subscribe`` + are set to ``True`` or ``False``, and not ``None``, then SleekXMPP will + either accept or reject all subscription requests before your event handlers + are called. Set these values to ``None`` if you wish to make more complex + subscription decisions. + + chatstate_active + - **Data:** + - **Source:** + + chatstate_composing + - **Data:** + - **Source:** + + chatstate_gone + - **Data:** + - **Source:** + + chatstate_inactive + - **Data:** + - **Source:** + + chatstate_paused + - **Data:** + - **Source:** + + disco_info + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0030.stanza.DiscoInfo` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0030.disco.xep_0030` + + Triggered whenever a ``disco#info`` result stanza is received. + + disco_items + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0030.stanza.DiscoItems` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0030.disco.xep_0030` + + Triggered whenever a ``disco#items`` result stanza is received. + + disconnected + - **Data:** ``{}`` + - **Source:** :py:class:`~sleekxmpp.ClientXMPP` + + Signal that the connection with the XMPP server has been lost. + + entity_time + - **Data:** + - **Source:** + + failed_auth + - **Data:** ``{}`` + - **Source:** :py:class:`~sleekxmpp.ClientXMPP`, :py:class:`~sleekxmpp.plugins.xep_0078.xep_0078` + + Signal that the server has rejected the provided login credentials. + + gmail_notify + - **Data:** ``{}`` + - **Source:** :py:class:`~sleekxmpp.plugins.gmail_notify.gmail_notify` + + Signal that there are unread emails for the Gmail account associated with the current XMPP account. + + gmail_messages + - **Data:** :py:class:`~sleekxmpp.Iq` + - **Source:** :py:class:`~sleekxmpp.plugins.gmail_notify.gmail_notify` + + Signal that there are unread emails for the Gmail account associated with the current XMPP account. + + got_online + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + If a presence stanza is received from a JID which was previously marked as + offline, and the presence has a show type of '``chat``', '``dnd``', '``away``', + or '``xa``', then this event is triggered as well. + + got_offline + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + Signal that an unavailable presence stanza has been received from a JID. + + groupchat_invite + - **Data:** + - **Source:** + + groupchat_direct_invite + - **Data:** :py:class:`~sleekxmpp.Message` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0249.direct` + + groupchat_message + - **Data:** :py:class:`~sleekxmpp.Message` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045` + + Triggered whenever a message is received from a multi-user chat room. + + groupchat_presence + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045` + + Triggered whenever a presence stanza is received from a user in a multi-user chat room. + + groupchat_subject + - **Data:** :py:class:`~sleekxmpp.Message` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045` + + Triggered whenever the subject of a multi-user chat room is changed, or announced when joining a room. + + killed + - **Data:** + - **Source:** + + last_activity + - **Data:** + - **Source:** + + message + - **Data:** :py:class:`~sleekxmpp.Message` + - **Source:** :py:class:`BaseXMPP ` + + Makes the contents of message stanzas available whenever one is received. Be + sure to check the message type in order to handle error messages. + + message_form + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + + Currently the same as :term:`message_xform`. + + message_xform + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + + Triggered whenever a data form is received inside a message. + + mucc::[room]::got_offline + - **Data:** + - **Source:** + + muc::[room]::got_online + - **Data:** + - **Source:** + + muc::[room]::message + - **Data:** + - **Source:** + + muc::[room]::presence + - **Data:** + - **Source:** + + presence_available + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``available``' is received. + + presence_error + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``error``' is received. + + presence_form + - **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form` + - **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004` + + This event is present in the XEP-0004 plugin code, but is currently not used. + + presence_probe + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``probe``' is received. + + presence_subscribe + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``subscribe``' is received. + + presence_subscribed + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``subscribed``' is received. + + presence_unavailable + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``unavailable``' is received. + + presence_unsubscribe + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``unsubscribe``' is received. + + presence_unsubscribed + - **Data:** :py:class:`~sleekxmpp.Presence` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` + + A presence stanza with a type of '``unsubscribed``' is received. + + roster_update + - **Data:** :py:class:`~sleekxmpp.stanza.Roster` + - **Source:** :py:class:`~sleekxmpp.ClientXMPP` + + An IQ result containing roster entries is received. + + sent_presence + - **Data:** ``{}`` + - **Source:** :py:class:`BaseXMPP ` + + Signal that an initial presence stanza has been written to the XML stream. + + session_end + - **Data:** ``{}`` + - **Source:** :py:class:`ClientXMPP `, + :py:class:`ComponentXMPP ` + :py:class:`XEP-0078 ` + + Signal that a connection to the XMPP server has been lost and the current + stream session has ended. Currently equivalent to :term:`disconnected`, but + future implementation of `XEP-0198: Stream Management `_ + will distinguish the two events. + + Plugins that maintain session-based state should clear themselves when + this event is fired. + + session_start + - **Data:** ``{}`` + - **Source:** :py:class:`ClientXMPP `, + :py:class:`ComponentXMPP ` + :py:class:`XEP-0078 ` + + Signal that a connection to the XMPP server has been made and a session has been established. + + socket_error + - **Data:** ``Socket`` exception object + - **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream` + + stream_error + - **Data:** :py:class:`~sleekxmpp.stanza.StreamError` + - **Source:** :py:class:`~sleekxmpp.BaseXMPP` diff --git a/docs/features.rst b/docs/features.rst new file mode 100644 index 0000000..4d93d5c --- /dev/null +++ b/docs/features.rst @@ -0,0 +1,2 @@ +How to Use Stream Features +========================== diff --git a/docs/getting_started/component.rst b/docs/getting_started/component.rst new file mode 100644 index 0000000..ce548ba --- /dev/null +++ b/docs/getting_started/component.rst @@ -0,0 +1,75 @@ +.. _echocomponent: + +================================= +Create and Run a Server Component +================================= + +.. note:: + + If you have any issues working through this quickstart guide + or the other tutorials here, please either send a message to the + `mailing list `_ + or join the chat room at `sleek@conference.jabber.org + `_. + +If you have not yet installed SleekXMPP, do so now by either checking out a version +from `Github `_, or installing it using ``pip`` +or ``easy_install``. + +.. code-block:: sh + + pip install sleekxmpp # Or: easy_install sleekxmpp + + +Many XMPP applications eventually graduate to requiring to run as a server +component in order to meet scalability requirements. To demonstrate how to +turn an XMPP client bot into a component, we'll turn the echobot example +(:ref:`echobot`) into a component version. + +The first difference is that we will add an additional import statement: + +.. code-block:: python + + from sleekxmpp.componentxmpp import ComponentXMPP + +Likewise, we will change the bot's class definition to match: + +.. code-block:: python + + class EchoComponent(ComponentXMPP): + + def __init__(self, jid, secret, server, port): + ComponentXMPP.__init__(self, jid, secret, server, port) + +A component instance requires two extra parameters compared to a client +instance: ``server`` and ``port``. These specifiy the name and port of +the XMPP server that will be accepting the component. For example, for +a MUC component, the following could be used: + +.. code-block:: python + + muc = ComponentXMPP('muc.sleekxmpp.com', '******', 'sleekxmpp.com', 5555) + +.. note:: + + The ``server`` value is **NOT** derived from the provided JID for the + component, unlike with client connections. + +One difference with the component version is that we do not have +to handle the :term:`session_start` event if we don't wish to deal +with presence. + +The other, main difference with components is that the +``'from'`` value for every stanza must be explicitly set, since +components may send stanzas from multiple JIDs. To do so, +the :meth:`~sleekxmpp.basexmpp.BaseXMPP.send_message()` and +:meth:`~sleekxmpp.basexmpp.BaseXMPP.send_presence()` accept the parameters +``mfrom`` and ``pfrom``, respectively. For any method that uses +:class:`~sleekxmpp.stanza.iq.Iq` stanzas, ``ifrom`` may be used. + + +Final Product +------------- + +.. include:: ../../examples/echo_component.py + :literal: diff --git a/docs/getting_started/echobot.rst b/docs/getting_started/echobot.rst new file mode 100644 index 0000000..053a76f --- /dev/null +++ b/docs/getting_started/echobot.rst @@ -0,0 +1,390 @@ +.. _echobot: + +=============================== +SleekXMPP Quickstart - Echo Bot +=============================== + +.. note:: + + If you have any issues working through this quickstart guide + or the other tutorials here, please either send a message to the + `mailing list `_ + or join the chat room at `sleek@conference.jabber.org + `_. + +If you have not yet installed SleekXMPP, do so now by either checking out a version +from `Github `_, or installing it using ``pip`` +or ``easy_install``. + +.. code-block:: sh + + pip install sleekxmpp # Or: easy_install sleekxmpp + + +As a basic starting project, we will create an echo bot which will reply to any +messages sent to it. We will also go through adding some basic command line configuration +for enabling or disabling debug log outputs and setting the username and password +for the bot. + +For the command line options processing, we will use the built-in ``optparse`` +module and the ``getpass`` module for reading in passwords. + +TL;DR Just Give Me the Code +--------------------------- +As you wish: :ref:`the completed example `. + +Overview +-------- + +To get started, here is a brief outline of the structure that the final project will have: + +.. code-block:: python + + #!/usr/bin/env python + # -*- coding: utf-8 -*- + + import sys + import logging + import getpass + from optparse import OptionParser + + import sleekxmpp + + '''Here we will create out echo bot class''' + + if __name__ == '__main__': + '''Here we will configure and read command line options''' + + '''Here we will instantiate our echo bot''' + + '''Finally, we connect the bot and start listening for messages''' + +Default Encoding +---------------- +XMPP requires support for UTF-8 and so SleekXMPP must use UTF-8 as well. In +Python3 this is simple because Unicode is the default string type. For Python2.6+ +the situation is not as easy because standard strings are simply byte arrays and +use ASCII. We can get Python to use UTF-8 as the default encoding by including: + +.. code-block:: python + + if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') + +.. warning:: + + Until we are able to ensure that SleekXMPP will always use Unicode in Python2.6+, this + may cause issues embedding SleekXMPP into other applications which assume ASCII encoding. + +Creating the EchoBot Class +-------------------------- + +There are three main types of entities within XMPP — servers, components, and +clients. Since our echo bot will only be responding to a few people, and won't need +to remember thousands of users, we will use a client connection. A client connection +is the same type that you use with your standard IM client such as Pidgin or Psi. + +SleekXMPP comes with a :class:`ClientXMPP ` class +which we can extend to add our message echoing feature. :class:`ClientXMPP ` +requires the parameters ``jid`` and ``password``, so we will let our ``EchoBot`` class accept those +as well. + +.. code-block:: python + + class EchoBot(sleekxmpp.ClientXMPP): + + def __init__(self, jid, password): + super(EchoBot, self).__init__(jid, password) + +Handling Session Start +~~~~~~~~~~~~~~~~~~~~~~ +The XMPP spec requires clients to broadcast its presence and retrieve its roster (buddy list) once +it connects and establishes a session with the XMPP server. Until these two tasks are completed, +some servers may not deliver or send messages or presence notifications to the client. So we now +need to be sure that we retrieve our roster and send an initial presence once the session has +started. To do that, we will register an event handler for the :term:`session_start` event. + +.. code-block:: python + + def __init__(self, jid, password): + super(EchoBot, self).__init__(jid, password) + + self.add_event_handler('session_start', self.start) + + +Since we want the method ``self.start`` to execute when the :term:`session_start` event is triggered, +we also need to define the ``self.start`` handler. + +.. code-block:: python + + def start(self, event): + self.send_presence() + self.get_roster() + +.. warning:: + + Not sending an initial presence and retrieving the roster when using a client instance can + prevent your program from receiving presence notifications or messages depending on the + XMPP server you have chosen. + +Our event handler, like every event handler, accepts a single parameter which typically is the stanza +that was received that caused the event. In this case, ``event`` will just be an empty dictionary since +there is no associated data. + +Our first task of sending an initial presence is done using :meth:`send_presence `. +Calling :meth:`send_presence ` without any arguments will send the simplest +stanza allowed in XMPP: + +.. code-block:: xml + + + + +The second requirement is fulfilled using :meth:`get_roster `, which +will send an IQ stanza requesting the roster to the server and then wait for the response. You may be wondering +what :meth:`get_roster ` returns since we are not saving any return +value. The roster data is saved by an internal handler to ``self.roster``, and in the case of a :class:`ClientXMPP +` instance to ``self.client_roster``. (The difference between ``self.roster`` and +``self.client_roster`` is that ``self.roster`` supports storing roster information for multiple JIDs, which is useful +for components, whereas ``self.client_roster`` stores roster data for just the client's JID.) + +It is possible for a timeout to occur while waiting for the server to respond, which can happen if the +network is excessively slow or the server is no longer responding. In that case, an :class:`IQTimeout +` is raised. Similarly, an :class:`IQError ` exception can +be raised if the request contained bad data or requested the roster for the wrong user. In either case, you can wrap the +``get_roster()`` call in a ``try``/``except`` block to retry the roster retrieval process. + +The XMPP stanzas from the roster retrieval process could look like this: + +.. code-block:: xml + + + + + + + + + + + +Responding to Messages +~~~~~~~~~~~~~~~~~~~~~~ +Now that an ``EchoBot`` instance handles :term:`session_start`, we can begin receiving and +responding to messages. Now we can register a handler for the :term:`message` event that is raised +whenever a messsage is received. + +.. code-block:: python + + def __init__(self, jid, password): + super(EchoBot, self).__init__(jid, password) + + self.add_event_handler('session_start', self.start) + self.add_event_handler('message', self.message) + + +The :term:`message` event is fired whenever a ```` stanza is received, including for +group chat messages, errors, etc. Properly responding to messages thus requires checking the +``'type'`` interface of the message :term:`stanza object`. For responding to only messages +addressed to our bot (and not from a chat room), we check that the type is either ``normal`` +or ``chat``. (Other potential types are ``error``, ``headline``, and ``groupchat``.) + +.. code-block:: python + + def message(self, msg): + if msg['type'] in ('normal', 'chat'): + msg.reply("Thanks for sending:\n%s" % msg['body']).send() + +Let's take a closer look at the ``.reply()`` method used above. For message stanzas, +``.reply()`` accepts the parameter ``body`` (also as the first positional argument), +which is then used as the value of the ```` element of the message. +Setting the appropriate ``to`` JID is also handled by ``.reply()``. + +Another way to have sent the reply message would be to use :meth:`send_message `, +which is a convenience method for generating and sending a message based on the values passed to it. If we were to use +this method, the above code would look as so: + +.. code-block:: python + + def message(self, msg): + if msg['type'] in ('normal', 'chat'): + self.send_message(mto=msg['from'], + mbody='Thanks for sending:\n%s' % msg['body']) + +Whichever method you choose to use, the results in action will look like this: + +.. code-block:: xml + + + Hej! + + + + Thanks for sending: + Hej! + + +.. note:: + XMPP does not require stanzas sent by a client to include a ``from`` attribute, and + leaves that responsibility to the XMPP server. However, if a sent stanza does + include a ``from`` attribute, it must match the full JID of the client or some + servers will reject it. SleekXMPP thus leaves out the ``from`` attribute when replying + using a client connection. + +Command Line Arguments and Logging +---------------------------------- + +While this isn't part of SleekXMPP itself, we do want our echo bot program to be able +to accept a JID and password from the command line instead of hard coding them. We will +use the ``optparse`` module for this, though there are several alternative methods, including +the newer ``argparse`` module. + +We want to accept three parameters: the JID for the echo bot, its password, and a flag for +displaying the debugging logs. We also want these to be optional parameters, since passing +a password directly through the command line can be a security risk. + +.. code-block:: python + + if __name__ == '__main__': + optp = OptionParser() + + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + + opts, args = optp.parse_args() + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + +Since we included a flag for enabling debugging logs, we need to configure the +``logging`` module to behave accordingly. + +.. code-block:: python + + if __name__ == '__main__': + + # .. option parsing from above .. + + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + +Connecting to the Server and Processing +--------------------------------------- +There are three steps remaining until our echo bot is complete: + 1. We need to instantiate the bot. + 2. The bot needs to connect to an XMPP server. + 3. We have to instruct the bot to start running and processing messages. + +Creating the bot is straightforward, but we can also perform some configuration +at this stage. For example, let's say we want our bot to support `service discovery +`_ and `pings `_: + +.. code-block:: python + + if __name__ == '__main__': + + # .. option parsing and logging steps from above + + xmpp = EchoBot(opts.jid, opts.password) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0199') # Ping + +If the ``EchoBot`` class had a hard dependency on a plugin, we could register that plugin in +the ``EchoBot.__init__`` method instead. + +.. note:: + + If you are using the OpenFire server, you will need to include an additional + configuration step. OpenFire supports a different version of SSL than what + most servers and SleekXMPP support. + + .. code-block:: python + + import ssl + xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + +Now we're ready to connect and begin echoing messages. If you have the package +``dnspython`` installed, then the :meth:`sleekxmpp.clientxmpp.ClientXMPP` method +will perform a DNS query to find the appropriate server to connect to for the +given JID. If you do not have ``dnspython``, then SleekXMPP will attempt to +connect to the hostname used by the JID, unless an address tuple is supplied +to :meth:`sleekxmpp.clientxmpp.ClientXMPP`. + +.. code-block:: python + + if __name__ == '__main__': + + # .. option parsing & echo bot configuration + + if xmpp.connect(): + xmpp.process(block=True) + else: + print('Unable to connect') + +.. note:: + + For Google Talk users withouth ``dnspython`` installed, the above code + should look like: + + .. code-block:: python + + if __name__ == '__main__': + + # .. option parsing & echo bot configuration + + if xmpp.connect(('talk.google.com', 5222)): + xmpp.process(block=True) + else: + print('Unable to connect') + +To begin responding to messages, you'll see we called :meth:`sleekxmpp.basexmpp.BaseXMPP.process` +which will start the event handling, send queue, and XML reader threads. It will also call +the :meth:`sleekxmpp.plugins.base.base_plugin.post_init` method on all registered plugins. By +passing ``block=True`` to :meth:`sleekxmpp.basexmpp.BaseXMPP.process` we are running the +main processing loop in the main thread of execution. The :meth:`sleekxmpp.basexmpp.BaseXMPP.process` +call will not return until after SleekXMPP disconnects. If you need to run the client in the background +for another program, use ``block=False`` to spawn the processing loop in its own thread. + +.. note:: + + Before 1.0, controlling the blocking behaviour of :meth:`sleekxmpp.basexmpp.BaseXMPP.process` was + done via the ``threaded`` argument. This arrangement was a source of confusion because some users + interpreted that as controlling whether or not SleekXMPP used threads at all, instead of how + the processing loop itself was spawned. + + The statements ``xmpp.process(threaded=False)`` and ``xmpp.process(block=True)`` are equivalent. + + +.. _echobot_complete: + +The Final Product +----------------- + +Here then is what the final result should look like after working through the guide above. The code +can also be found in the SleekXMPP `examples directory `_. + +.. compound:: + + You can run the code using: + + .. code-block:: sh + + python echobot.py -d -j echobot@example.com + + which will prompt for the password and then begin echoing messages. To test, open + your regular IM client and start a chat with the echo bot. Messages you send to it should + be mirrored back to you. Be careful if you are using the same JID for the echo bot that + you also have logged in with another IM client. Messages could be routed to your IM client instead + of the bot. + +.. include:: ../../examples/echo_client.py + :literal: diff --git a/docs/getting_started/iq.rst b/docs/getting_started/iq.rst new file mode 100644 index 0000000..98e0bda --- /dev/null +++ b/docs/getting_started/iq.rst @@ -0,0 +1,182 @@ +Send/Receive IQ Stanzas +======================= + +Unlike :class:`~sleekxmpp.stanza.message.Message` and +:class:`~sleekxmpp.stanza.presence.Presence` stanzas which only use +text data for basic usage, :class:`~sleekxmpp.stanza.iq.Iq` stanzas +require using XML payloads, and generally entail creating a new +SleekXMPP plugin to provide the necessary convenience methods to +make working with them easier. + +Basic Use +--------- + +XMPP's use of :class:`~sleekxmpp.stanza.iq.Iq` stanzas is built around +namespaced ```` elements. For clients, just sending the +empty ```` element will suffice for retrieving information. For +example, a very basic implementation of service discovery would just +need to be able to send: + +.. code-block:: xml + + + + + +Creating Iq Stanzas +~~~~~~~~~~~~~~~~~~~ + +SleekXMPP provides built-in support for creating basic :class:`~sleekxmpp.stanza.iq.Iq` +stanzas this way. The relevant methods are: + +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq` +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_get` +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_set` +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_result` +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_error` +* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_query` + +These methods all follow the same pattern: create or modify an existing +:class:`~sleekxmpp.stanza.iq.Iq` stanza, set the ``'type'`` value based +on the method name, and finally add a ```` element with the given +namespace. For example, to produce the query above, you would use: + +.. code-block:: python + + self.make_iq_get(queryxmlns='http://jabber.org/protocol/disco#info', + ito='user@example.com') + + +Sending Iq Stanzas +~~~~~~~~~~~~~~~~~~ + +Once an :class:`~sleekxmpp.stanza.iq.Iq` stanza is created, sending it +over the wire is done using its :meth:`~sleekxmpp.stanza.iq.Iq.send()` +method, like any other stanza object. However, there are a few extra +options to control how to wait for the query's response. + +These options are: + +* ``block``: The default behaviour is that :meth:`~sleekxmpp.stanza.iq.Iq.send()` + will block until a response is received and the response stanza will be the + return value. Setting ``block`` to ``False`` will cause the call to return + immediately. In which case, you will need to arrange some way to capture + the response stanza if you need it. + +* ``timeout``: When using the blocking behaviour, the call will eventually + timeout with an error. The default timeout is 30 seconds, but this may + be overidden two ways. To change the timeout globally, set: + + .. code-block:: python + + self.response_timeout = 10 + + To change the timeout for a single call, the ``timeout`` parameter works: + + .. code-block:: python + + iq.send(timeout=60) + +* ``callback``: When not using a blocking call, using the ``callback`` + argument is a simple way to register a handler that will execute + whenever a response is finally received. Using this method, there + is no timeout limit. In case you need to remove the callback, the + name of the newly created callback is returned. + + .. code-block:: python + + cb_name = iq.send(callback=self.a_callback) + + # ... later if we need to cancel + self.remove_handler(cb_name) + +Properly working with :class:`~sleekxmpp.stanza.iq.Iq` stanzas requires +handling the intended, normal flow, error responses, and timed out +requests. To make this easier, two exceptions may be thrown by +:meth:`~sleekxmpp.stanza.iq.Iq.send()`: :exc:`~sleekxmpp.exceptions.IqError` +and :exc:`~sleekxmpp.exceptions.IqTimeout`. These exceptions only +apply to the default, blocking calls. + +.. code-block:: python + + try: + resp = iq.send() + # ... do stuff with expected Iq result + except IqError as e: + err_resp = e.iq + # ... handle error case + except IqTimeout: + # ... no response received in time + pass + +If you do not care to distinguish between errors and timeouts, then you +can combine both cases with a generic :exc:`~sleekxmpp.exceptions.XMPPError` +exception: + +.. code-block:: python + + try: + resp = iq.send() + except XMPPError: + # ... Don't care about the response + pass + +Advanced Use +------------ + +Going beyond the basics provided by SleekXMPP requires building at least a +rudimentary SleekXMPP plugin to create a :term:`stanza object` for +interfacting with the :class:`~sleekxmpp.stanza.iq.Iq` payload. + +.. seealso:: + + * :ref:`create-plugin` + * :ref:`work-with-stanzas` + * :ref:`using-handlers-matchers` + + +The typical way to respond to :class:`~sleekxmpp.stanza.iq.Iq` requests is +to register stream handlers. As an example, suppose we create a stanza class +named ``CustomXEP`` which uses the XML element ````, +and has a :attr:`~sleekxmpp.xmlstream.stanzabase.ElementBase.plugin_attrib` value +of ``custom_xep``. + +There are two types of incoming :class:`~sleekxmpp.stanza.iq.Iq` requests: +``get`` and ``set``. You can register a handler that will accept both and then +filter by type as needed, as so: + +.. code-block:: python + + self.register_handler(Callback( + 'CustomXEP Handler', + StanzaPath('iq/custom_xep'), + self._handle_custom_iq)) + + # ... + + def _handle_custom_iq(self, iq): + if iq['type'] == 'get': + # ... + pass + elif iq['type'] == 'set': + # ... + pass + else: + # ... This will capture error responses too + pass + +If you want to filter out query types beforehand, you can adjust the matching +filter by using ``@type=get`` or ``@type=set`` if you are using the recommended +:class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher. + +.. code-block:: python + + self.register_handler(Callback( + 'CustomXEP Handler', + StanzaPath('iq@type=get/custom_xep'), + self._handle_custom_iq_get)) + + # ... + + def _handle_custom_iq_get(self, iq): + assert(iq['type'] == 'get') diff --git a/docs/getting_started/muc.rst b/docs/getting_started/muc.rst new file mode 100644 index 0000000..08f721f --- /dev/null +++ b/docs/getting_started/muc.rst @@ -0,0 +1,2 @@ +Mulit-User Chat (MUC) Bot +========================= diff --git a/docs/getting_started/presence.rst b/docs/getting_started/presence.rst new file mode 100644 index 0000000..e070e81 --- /dev/null +++ b/docs/getting_started/presence.rst @@ -0,0 +1,2 @@ +Manage Presence Subscriptions +============================= diff --git a/docs/getting_started/proxy.rst b/docs/getting_started/proxy.rst new file mode 100644 index 0000000..60d521c --- /dev/null +++ b/docs/getting_started/proxy.rst @@ -0,0 +1,42 @@ +.. _proxy: + +========================= +Enable HTTP Proxy Support +========================= + +.. note:: + + If you have any issues working through this quickstart guide + or the other tutorials here, please either send a message to the + `mailing list `_ + or join the chat room at `sleek@conference.jabber.org + `_. + +In some instances, you may wish to route XMPP traffic through +an HTTP proxy, probably to get around restrictive firewalls. +SleekXMPP provides support for basic HTTP proxying with DIGEST +authentication. + +Enabling proxy support is done in two steps. The first is to instruct SleekXMPP +to use a proxy, and the second is to configure the proxy details: + +.. code-block:: python + + xmpp = ClientXMPP(...) + xmpp.use_proxy = True + xmpp.proxy_config = { + 'host': 'proxy.example.com', + 'port': 5555, + 'username': 'example_user', + 'password': '******' + } + +The ``'username'`` and ``'password'`` fields are optional if the proxy does not +require authentication. + + +The Final Product +----------------- + +.. include:: ../../examples/proxy_echo_client.py + :literal: diff --git a/docs/getting_started/scheduler.rst b/docs/getting_started/scheduler.rst new file mode 100644 index 0000000..a9263a1 --- /dev/null +++ b/docs/getting_started/scheduler.rst @@ -0,0 +1,2 @@ +Send a Message Every 5 Minutes +============================== diff --git a/docs/getting_started/sendlogout.rst b/docs/getting_started/sendlogout.rst new file mode 100644 index 0000000..a1352db --- /dev/null +++ b/docs/getting_started/sendlogout.rst @@ -0,0 +1,94 @@ +Sign in, Send a Message, and Disconnect +======================================= + +.. note:: + + If you have any issues working through this quickstart guide + or the other tutorials here, please either send a message to the + `mailing list `_ + or join the chat room at `sleek@conference.jabber.org + `_. + +A common use case for SleekXMPP is to send one-off messages from +time to time. For example, one use case could be sending out a notice when +a shell script finishes a task. + +We will create our one-shot bot based on the pattern explained in :ref:`echobot`. To +start, we create a client class based on :class:`ClientXMPP ` and +register a handler for the :term:`session_start` event. We will also accept parameters +for the JID that will receive our message, and the string content of the message. + +.. code-block:: python + + import sleekxmpp + + + class SendMsgBot(sleekxmpp.ClientXMPP): + + def __init__(self, jid, password, recipient, msg): + super(SendMsgBot, self).__init__(jid, password) + + self.recipient = recipient + self.msg = msg + + self.add_event_handler('session_start', self.start) + + def start(self, event): + self.send_presence() + self.get_roster() + +Note that as in :ref:`echobot`, we need to include send an initial presence and request +the roster. Next, we want to send our message, and to do that we will use :meth:`send_message `. + +.. code-block:: python + + def start(self, event): + self.send_presence() + self.get_roster() + + self.send_message(mto=self.recipient, mbody=self.msg) + +Finally, we need to disconnect the client using :meth:`disconnect `. +Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call +:meth:`disconnect ` without any parameters, then it is possible +for the client to disconnect before the send queue is processed and the message is actually +sent on the wire. To ensure that our message is processed, we use +:meth:`disconnect(wait=True) `. + +.. code-block:: python + + def start(self, event): + self.send_presence() + self.get_roster() + + self.send_message(mto=self.recipient, mbody=self.msg) + + self.disconnect(wait=True) + +.. warning:: + + If you happen to be adding stanzas to the send queue faster than the send thread + can process them, then :meth:`disconnect(wait=True) ` + will block and not disconnect. + +Final Product +------------- + +.. compound:: + + The final step is to create a small runner script for initialising our ``SendMsgBot`` class and adding some + basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive + at the code below. To experiment with this example, you can use: + + .. code-block:: sh + + python send_client.py -d -j oneshot@example.com -t someone@example.net -m "This is a message" + + which will prompt for the password and then log in, send your message, and then disconnect. To test, open + your regular IM client with the account you wish to send messages to. When you run the ``send_client.py`` + example and instruct it to send your IM client account a message, you should receive the message you + gave. If the two JIDs you use also have a mutual presence subscription (they're on each other's buddy lists) + then you will also see the ``SendMsgBot`` client come online and then go offline. + +.. include:: ../../examples/send_client.py + :literal: diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..35d2dc8 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,35 @@ +.. _glossary: + +Glossary +======== + +.. glossary:: + :sorted: + + stream handler + A callback function that accepts stanza objects pulled directly + from the XML stream. A stream handler is encapsulated in a + object that includes a :term:`Matcher` object, and which provides + additional semantics. For example, the ``Waiter`` handler wrapper + blocks thread execution until a matching stanza is received. + + event handler + A callback function that responds to events raised by + ``XMLStream.event``. An event handler may be marked as + threaded, allowing it to execute outside of the main processing + loop. + + stanza object + Informally may refer both to classes which extend ``ElementBase`` + or ``StanzaBase``, and to objects of such classes. + + A stanza object is a wrapper for an XML object which exposes ``dict`` + like interfaces which may be assigned to, read from, or deleted. + + stanza plugin + A :term:`stanza object` which has been registered as a potential child + of another stanza object. The plugin stanza may accessed through the + parent stanza using the plugin's ``plugin_attrib`` as an interface. + + substanza + See :term:`stanza plugin` diff --git a/docs/guide_xep_0030.rst b/docs/guide_xep_0030.rst new file mode 100644 index 0000000..cb8d7d2 --- /dev/null +++ b/docs/guide_xep_0030.rst @@ -0,0 +1,201 @@ +XEP-0030: Working with Service Discovery +======================================== + +XMPP networks can be composed of many individual clients, components, +and servers. Determining the JIDs for these entities and the various +features they may support is the role of `XEP-0030, Service +Discovery `_, or "disco" for short. + +Every XMPP entity may possess what are called nodes. A node is just a name for +some aspect of an XMPP entity. For example, if an XMPP entity provides `Ad-Hoc +Commands `_, then it will have a node +named ``http://jabber.org/protocol/commands`` which will contain information +about the commands provided. Other agents using these ad-hoc commands will +interact with the information provided by this node. Note that the node name is +just an identifier; there is no inherent meaning. + +Working with service discovery is about creating and querying these nodes. +According to XEP-0030, a node may contain three types of information: +identities, features, and items. (Further, extensible, information types are +defined in `XEP-0128 `_, but they are +not yet implemented by SleekXMPP.) SleekXMPP provides methods to configure each +of these node attributes. + +Configuring Service Discovery +----------------------------- +The design focus for the XEP-0030 plug-in is handling info and items requests +in a dynamic fashion, allowing for complex policy decisions of who may receive +information and how much, or use alternate backend storage mechanisms for all +of the disco data. To do this, each action that the XEP-0030 plug-in performs +is handed off to what is called a "node handler," which is just a callback +function. These handlers are arranged in a hierarchy that allows for a single +handler to manage an entire domain of JIDs (say for a component), while allowing +other handler functions to override that global behaviour for certain JIDs, or +even further limited to only certain JID and node combinations. + +The Dynamic Handler Hierarchy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* ``global``: (JID is None, node is None) + + Handlers assigned at this level for an action (such as ``add_feature``) provide a global default + behaviour when the action is performed. + +* ``jid``: (JID assigned, node is None) + + At this level, handlers provide a default behaviour for actions affecting any node owned by the + JID in question. This level is most useful for component connections; there is effectively no + difference between this and the global level when using a client connection. + +* ``node``: (JID assigned, node assigned) + + A handler for this level is responsible for carrying out an action for only one node, and is the + most specific handler type available. These types of handlers will be most useful for "special" + nodes that require special processing different than others provided by the JID, such as using + access control lists, or consolidating data from other nodes. + +Default Static Handlers +~~~~~~~~~~~~~~~~~~~~~~~ +The XEP-0030 plug-in provides a default set of handlers that work using in-memory +disco stanzas. Each handler simply performs the appropriate lookup or storage +operation using these stanzas without doing any complex operations such as +checking an ACL, etc. + +You may find it necessary at some point to revert a particular node or JID to +using the default, static handlers. To do so, use the method ``make_static()``. +You may also elect to only convert a given set of actions instead. + +Creating a Node Handler +~~~~~~~~~~~~~~~~~~~~~~~ +Every node handler receives three arguments: the JID, the node, and a data +parameter that will contain the relevant information for carrying out the +handler's action, typically a dictionary. + +The JID will always have a value, defaulting to ``xmpp.boundjid.full`` for +components or ``xmpp.boundjid.bare`` for clients. The node value may be None or +a string. + +Only handlers for the actions ``get_info`` and ``get_items`` need to have return +values. For these actions, DiscoInfo or DiscoItems stanzas are exepected as +output. It is also acceptable for handlers for these actions to generate an +XMPPError exception when necessary. + +Example Node Handler: ++++++++++++++++++++++ +Here is one of the built-in default handlers as an example: + +.. code-block:: python + + def add_identity(self, jid, node, data): + """ + Add a new identity to the JID/node combination. + + The data parameter may provide: + category -- The general category to which the agent belongs. + itype -- A more specific designation with the category. + name -- Optional human readable name for this identity. + lang -- Optional standard xml:lang value. + """ + self.add_node(jid, node) + self.nodes[(jid, node)]['info'].add_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) + +Adding Identities, Features, and Items +-------------------------------------- +In order to maintain some backwards compatibility, the methods ``add_identity``, +``add_feature``, and ``add_item`` do not follow the method signature pattern of +the other API methods (i.e. jid, node, then other options), but rather retain +the parameter orders from previous plug-in versions. + +Adding an Identity +~~~~~~~~~~~~~~~~~~ +Adding an identity may be done using either the older positional notation, or +with keyword parameters. The example below uses the keyword arguments, but in +the same order as expected using positional arguments. + +.. code-block:: python + + xmpp['xep_0030'].add_identity(category='client', + itype='bot', + name='Sleek', + node='foo', + jid=xmpp.boundjid.full, + lang='no') + +The JID and node values determine which handler will be used to perform the +``add_identity`` action. + +The ``lang`` parameter allows for adding localized versions of identities using +the ``xml:lang`` attribute. + +Adding a Feature +~~~~~~~~~~~~~~~~ +The position ordering for ``add_feature()`` is to include the feature, then +specify the node and then the JID. The JID and node values determine which +handler will be used to perform the ``add_feature`` action. + +.. code-block:: python + + xmpp['xep_0030'].add_feature(feature='jabber:x:data', + node='foo', + jid=xmpp.boundjid.full) + +Adding an Item +~~~~~~~~~~~~~~ +The parameters to ``add_item()`` are potentially confusing due to the fact that +adding an item requires two JID and node combinations: the JID and node of the +item itself, and the JID and node that will own the item. + +.. code-block:: python + + xmpp['xep_0030'].add_item(jid='myitemjid@example.com', + name='An Item!', + node='owner_node', + subnode='item_node', + ijid=xmpp.boundjid.full) + +.. note:: + + In this case, the owning JID and node are provided with the + parameters ``ijid`` and ``node``. + +Peforming Disco Queries +----------------------- +The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs +and their nodes for disco information. Since these methods are wrappers for +sending Iq stanzas, they also accept all of the parameters of the ``Iq.send()`` +method. The ``get_items()`` method may also accept the boolean parameter +``iterator``, which when set to ``True`` will return an iterator object using +the `XEP-0059 `_ plug-in. + +.. code-block:: python + + info = self['xep_0030'].get_info(jid='foo@example.com', + node='bar', + ifrom='baz@mycomponent.example.com', + block=True, + timeout=30) + + items = self['xep_0030'].get_info(jid='foo@example.com', + node='bar', + iterator=True) + +For more examples on how to use basic disco queries, check the ``disco_browser.py`` +example in the ``examples`` directory. + +Local Queries +~~~~~~~~~~~~~ +In some cases, it may be necessary to query the contents of a node owned by the +client itself, or one of a component's many JIDs. The same method is used as for +normal queries, with two differences. First, the parameter ``local=True`` must +be used. Second, the return value will be a DiscoInfo or DiscoItems stanza, not +a full Iq stanza. + +.. code-block:: python + + info = self['xep_0030'].get_info(node='foo', local=True) + items = self['xep_0030'].get_items(jid='somejid@mycomponent.example.com', + node='bar', + local=True) diff --git a/docs/handlersmatchers.rst b/docs/handlersmatchers.rst new file mode 100644 index 0000000..628c414 --- /dev/null +++ b/docs/handlersmatchers.rst @@ -0,0 +1,4 @@ +.. _using-handlers-matchers: + +Using Stream Handlers and Matchers +================================== diff --git a/docs/howto/stanzas.rst b/docs/howto/stanzas.rst new file mode 100644 index 0000000..d52a90d --- /dev/null +++ b/docs/howto/stanzas.rst @@ -0,0 +1,30 @@ +.. _work-with-stanzas: + +How to Work with Stanza Objects +=============================== + + +.. _create-stanza-interfaces: + +Defining Stanza Interfaces +-------------------------- + + +.. _create-stanza-plugins: + +Creating Stanza Plugins +----------------------- + + + +.. _create-extension-plugins: + +Creating a Stanza Extension +--------------------------- + + + +.. _override-parent-interfaces: + +Overriding a Parent Stanza +-------------------------- diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..fc6541d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,179 @@ +SleekXMPP +######### + +.. sidebar:: Get the Code + + .. code-block:: sh + + pip install sleekxmpp + + The latest source code for SleekXMPP may be found on `Github + `_. Releases can be found in the + ``master`` branch, while the latest development version is in the + ``develop`` branch. + + **Latest Stable Release** + - `1.0 RC3 `_ + + **Develop Releases** + - `Latest Develop Version `_ + + + A mailing list and XMPP chat room are available for discussing and getting + help with SleekXMPP. + + **Mailing List** + `SleekXMPP Discussion on Google Groups `_ + + **Chat** + `sleek@conference.jabber.org `_ + + +SleekXMPP is an :ref:`MIT licensed ` XMPP library for Python 2.6/3.1+, +and is featured in examples in +`XMPP: The Definitive Guide `_ +by Kevin Smith, Remko Tronçon, and Peter Saint-Andre. If you've arrived +here from reading the Definitive Guide, please see the notes on updating +the examples to the latest version of SleekXMPP. + +SleekXMPP's design goals and philosphy are: + +**Low number of dependencies** + Installing and using SleekXMPP should be as simple as possible, without + having to deal with long dependency chains. + + As part of reducing the number of dependencies, some third party + modules are included with SleekXMPP in the ``thirdparty`` directory. + Imports from this module first try to import an existing installed + version before loading the packaged version, when possible. + +**Every XEP as a plugin** + Following Python's "batteries included" approach, the goal is to + provide support for all currently active XEPs (final and draft). Since + adding XEP support is done through easy to create plugins, the hope is + to also provide a solid base for implementing and creating experimental + XEPs. + +**Rewarding to work with** + As much as possible, SleekXMPP should allow things to "just work" using + sensible defaults and appropriate abstractions. XML can be ugly to work + with, but it doesn't have to be that way. + +Getting Started (with Examples) +------------------------------- +.. toctree:: + :maxdepth: 1 + + getting_started/echobot + getting_started/sendlogout + getting_started/component + getting_started/presence + getting_started/muc + getting_started/proxy + getting_started/scheduler + getting_started/iq + + +Tutorials, FAQs, and How To Guides +---------------------------------- +.. toctree:: + :maxdepth: 1 + + faq + xeps + xmpp_tdg + howto/stanzas + create_plugin + features + sasl + handlersmatchers + +Plugin Guides +~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + guide_xep_0030 + +SleekXMPP Architecture and Design +--------------------------------- +.. toctree:: + :maxdepth: 3 + + architecture + plugin_arch + +API Reference +------------- +.. toctree:: + :maxdepth: 2 + + event_index + api/clientxmpp + api/componentxmpp + api/basexmpp + api/exceptions + api/xmlstream/jid + api/xmlstream/stanzabase + api/xmlstream/handler + api/xmlstream/matcher + api/xmlstream/xmlstream + api/xmlstream/scheduler + api/xmlstream/tostring + api/xmlstream/filesocket + +Core Stanzas +~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 2 + + api/stanza/rootstanza + api/stanza/message + api/stanza/presence + api/stanza/iq + api/stanza/error + api/stanza/stream_error + +Plugins +~~~~~~~ +.. toctree:: + :maxdepth: 2 + + +Additional Info +--------------- +.. toctree:: + :hidden: + + glossary + license + +* :ref:`license` +* :ref:`glossary` +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +Credits +------- +**Main Author:** Nathan Fritz + `fritzy@netflint.net `_, + `@fritzy `_ + + Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP + `_, and a member of the XMPP + Council. + +**Co-Author:** Lance Stout + `lancestout@gmail.com `_, + `@lancestout `_ + +**Contributors:** + - Brian Beggs (`macdiesel `_) + - Dann Martens (`dannmartens `_) + - Florent Le Coz (`louiz `_) + - Kevin Smith (`Kev `_, http://kismith.co.uk) + - Remko Tronçon (`remko `_, http://el-tramo.be) + - Te-jé Rogers (`te-je `_) + - Thom Nichols (`tomstrummer `_) + diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..cbcf5c1 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,5 @@ +.. _license: + +License (MIT) +============= +.. include:: ../LICENSE diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..d97407a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,170 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + 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. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over 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 + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SleekXMPP.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SleekXMPP.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/plugin_arch.rst b/docs/plugin_arch.rst new file mode 100644 index 0000000..0141b79 --- /dev/null +++ b/docs/plugin_arch.rst @@ -0,0 +1,2 @@ +Plugin Architecture +=================== diff --git a/docs/python-objects.inv b/docs/python-objects.inv new file mode 100644 index 0000000..b7afc07 Binary files /dev/null and b/docs/python-objects.inv differ diff --git a/docs/sasl.rst b/docs/sasl.rst new file mode 100644 index 0000000..46c45c2 --- /dev/null +++ b/docs/sasl.rst @@ -0,0 +1,2 @@ +How SASL Authentication Works +============================= diff --git a/docs/xeps.rst b/docs/xeps.rst new file mode 100644 index 0000000..3653d10 --- /dev/null +++ b/docs/xeps.rst @@ -0,0 +1,50 @@ +Supported XEPS +============== + +======= ============================= ================ +XEP Description Notes +======= ============================= ================ +`0004`_ Data forms +`0009`_ Jabber RPC +`0012`_ Last Activity +`0030`_ Service Discovery +`0033`_ Extended Stanza Addressing +`0045`_ Multi-User Chat (MUC) Client-side only +`0050`_ Ad-hoc Commands +`0059`_ Result Set Management +`0060`_ Publish/Subscribe (PubSub) Client-side only +`0066`_ Out-of-band Data +`0078`_ Non-SASL Authentication +`0082`_ XMPP Date and Time Profiles +`0085`_ Chat-State Notifications +`0086`_ Error Condition Mappings +`0092`_ Software Version +`0128`_ Service Discovery Extensions +`0202`_ Entity Time +`0203`_ Delayed Delivery +`0224`_ Attention +`0249`_ Direct MUC Invitations +======= ============================= ================ + + +.. _0004: http://xmpp.org/extensions/xep-0004.html +.. _0009: http://xmpp.org/extensions/xep-0009.html +.. _0012: http://xmpp.org/extensions/xep-0012.html +.. _0030: http://xmpp.org/extensions/xep-0030.html +.. _0033: http://xmpp.org/extensions/xep-0033.html +.. _0045: http://xmpp.org/extensions/xep-0045.html +.. _0050: http://xmpp.org/extensions/xep-0050.html +.. _0059: http://xmpp.org/extensions/xep-0059.html +.. _0060: http://xmpp.org/extensions/xep-0060.html +.. _0066: http://xmpp.org/extensions/xep-0066.html +.. _0078: http://xmpp.org/extensions/xep-0078.html +.. _0082: http://xmpp.org/extensions/xep-0082.html +.. _0085: http://xmpp.org/extensions/xep-0085.html +.. _0086: http://xmpp.org/extensions/xep-0086.html +.. _0092: http://xmpp.org/extensions/xep-0092.html +.. _0128: http://xmpp.org/extensions/xep-0128.html +.. _0199: http://xmpp.org/extensions/xep-0199.html +.. _0202: http://xmpp.org/extensions/xep-0202.html +.. _0203: http://xmpp.org/extensions/xep-0203.html +.. _0224: http://xmpp.org/extensions/xep-0224.html +.. _0249: http://xmpp.org/extensions/xep-0249.html diff --git a/docs/xmpp_tdg.rst b/docs/xmpp_tdg.rst new file mode 100644 index 0000000..3d12b1b --- /dev/null +++ b/docs/xmpp_tdg.rst @@ -0,0 +1,249 @@ +Following *XMPP: The Definitive Guide* +====================================== + +SleekXMPP was featured in the first edition of the O'Reilly book +`XMPP: The Definitive Guide `_ +by Peter Saint-Andre, Kevin Smith, and Remko Tronçon. The original source code +for the book's examples can be found at http://github.com/remko/xmpp-tdg. An +updated version of the source code, maintained to stay current with the latest +SleekXMPP release, is available at http://github.com/legastero/xmpp-tdg. + +However, since publication, SleekXMPP has advanced from version 0.2.1 to version +1.0 and there have been several major API changes. The most notable is the +introduction of :term:`stanza objects ` which have simplified and +standardized interactions with the XMPP XML stream. + +What follows is a walk-through of *The Definitive Guide* highlighting the +changes needed to make the code examples work with version 1.0 of SleekXMPP. +These changes have been kept to a minimum to preserve the correlation with +the book's explanations, so be aware that some code may not use current best +practices. + +Example 2-2. (Page 26) +---------------------- + +**Implementation of a basic bot that echoes all incoming messages back to its sender.** + +The echo bot example requires a change to the ``handleIncomingMessage`` method +to reflect the use of the ``Message`` :term:`stanza object`. The +``"jid"`` field of the message object should now be ``"from"`` to match the +``from`` attribute of the actual XML message stanza. Likewise, ``"message"`` +changes to ``"body"`` to match the ``body`` element of the message stanza. + +Updated Code +~~~~~~~~~~~~ + +.. code-block:: python + + def handleIncomingMessage(self, message): + self.xmpp.sendMessage(message["from"], message["body"]) + +`View full source `_ | +`View original code `_ + +Example 14-1. (Page 215) +------------------------ + +**CheshiR IM bot implementation.** + +The main event handling method in the Bot class is meant to process both message +events and presence update events. With the new changes in SleekXMPP 1.0, +extracting a CheshiR status "message" from both types of stanzas +requires accessing different attributes. In the case of a message stanza, the +``"body"`` attribute would contain the CheshiR message. For a presence event, +the information is stored in the ``"status"`` attribute. To handle both cases, +we can test the type of the given event object and look up the proper attribute +based on the type. + +Like in the EchoBot example, the expression ``event["jid"]`` needs to change +to ``event["from"]`` in order to get a JID object for the stanza's sender. +Because other functions in CheshiR assume that the JID is a string, the ``jid`` +attribute is used to access the string version of the JID. A check is also added +in case ``user`` is ``None``, but the check could (and probably should) be +placed in ``addMessageFromUser``. + +Another change is needed in ``handleMessageAddedToBackend`` where +an HTML-IM response is created. The HTML content should be enclosed in a single +element, such as a ``

`` tag. + +Updated Code +~~~~~~~~~~~~ + +.. code-block:: python + + def handleIncomingXMPPEvent(self, event): + msgLocations = {sleekxmpp.stanza.presence.Presence: "status", + sleekxmpp.stanza.message.Message: "body"} + + message = event[msgLocations[type(event)]] + user = self.backend.getUserFromJID(event["from"].jid) + if user is not None: + self.backend.addMessageFromUser(message, user) + + def handleMessageAddedToBackend(self, message) : + body = message.user + ": " + message.text + htmlBody = "

%(user)s: %(message)s

" % { + "uri": self.url + "/" + message.user, + "user" : message.user, "message" : message.text } + for subscriberJID in self.backend.getSubscriberJIDs(message.user) : + self.xmpp.sendMessage(subscriberJID, body, mhtml=htmlBody) + +`View full source `_ | +`View original code `_ + + +Example 14-3. (Page 217) +------------------------ +**Configurable CheshiR IM bot implementation.** + +.. note:: + Since the CheshiR examples build on each other, see previous sections for + corrections to code that is not marked as new in the book example. + +The main difference for the configurable IM bot is the handling for the +data form in ``handleConfigurationCommand``. The test for equality +with the string ``"1"`` is no longer required; SleekXMPP converts +boolean data form fields to the values ``True`` and ``False`` +automatically. + +For the method ``handleIncomingXMPPPresence``, the attribute +``"jid"`` is again converted to ``"from"`` to get a JID +object for the presence stanza's sender, and the ``jid`` attribute is +used to access the string version of that JID object. A check is also added in +case ``user`` is ``None``, but the check could (and probably +should) be placed in ``getShouldMonitorPresenceFromUser``. + +Updated Code +~~~~~~~~~~~~ + +.. code-block:: python + + def handleConfigurationCommand(self, form, sessionId): + values = form.getValues() + monitorPresence =values["monitorPresence"] + jid = self.xmpp.plugin["xep_0050"].sessions[sessionId]["jid"] + user = self.backend.getUserFromJID(jid) + self.backend.setShouldMonitorPresenceFromUser(user, monitorPresence) + + def handleIncomingXMPPPresence(self, event): + user = self.backend.getUserFromJID(event["from"].jid) + if user is not None: + if self.backend.getShouldMonitorPresenceFromUser(user): + self.handleIncomingXMPPEvent(event) + +`View full source `_ | +`View original code `_ + + +Example 14-4. (Page 220) +------------------------ +**CheshiR IM server component implementation.** + +.. note:: + Since the CheshiR examples build on each other, see previous sections for + corrections to code that is not marked as new in the book example. + +Like several previous examples, a needed change is to replace +``subscription["from"]`` with ``subscription["from"].jid`` because the +``BaseXMPP`` method ``makePresence`` requires the JID to be a string. + +A correction needs to be made in ``handleXMPPPresenceProbe`` because a line was +left out of the original implementation; the variable ``user`` is undefined. The +JID of the user can be extracted from the presence stanza's ``from`` attribute. + +Since this implementation of CheshiR uses an XMPP component, it must +include a ``from`` attribute in all messages that it sends. Adding the +``from`` attribute is done by including ``mfrom=self.xmpp.jid`` in calls to +``self.xmpp.sendMessage``. + +Updated Code +~~~~~~~~~~~~ + +.. code-block:: python + + def handleXMPPPresenceProbe(self, event) : + self.xmpp.sendPresence(pto = event["from"]) + + def handleXMPPPresenceSubscription(self, subscription) : + if subscription["type"] == "subscribe" : + userJID = subscription["from"].jid + self.xmpp.sendPresenceSubscription(pto=userJID, ptype="subscribed") + self.xmpp.sendPresence(pto = userJID) + self.xmpp.sendPresenceSubscription(pto=userJID, ptype="subscribe") + + def handleMessageAddedToBackend(self, message) : + body = message.user + ": " + message.text + for subscriberJID in self.backend.getSubscriberJIDs(message.user) : + self.xmpp.sendMessage(subscriberJID, body, mfrom=self.xmpp.jid) + +`View full source `_ | +`View original code `_ + + +Example 14-6. (Page 223) +------------------------ +**CheshiR IM server component with in-band registration support.** + +.. note:: + Since the CheshiR examples build on each other, see previous sections for + corrections to code that is not marked as new in the book example. + +After applying the changes from Example 14-4 above, the registrable component +implementation should work correctly. + +.. tip:: + To see how to implement in-band registration as a SleekXMPP plugin, + see the tutorial :ref:`tutorial-create-plugin`. + +`View full source `_ | +`View original code `_ + +Example 14-7. (Page 225) +------------------------ +**Extended CheshiR IM server component implementation.** + +.. note:: + Since the CheshiR examples build on each other, see previous + sections for corrections to code that is not marked as new in the book + example. + +While the final code example can look daunting with all of the changes +made, it requires very few modifications to work with the latest version of +SleekXMPP. Most differences are the result of CheshiR's backend functions +expecting JIDs to be strings so that they can be stripped to bare JIDs. To +resolve these, use the ``jid`` attribute of the JID objects. Also, +references to ``"message"`` and ``"jid"`` attributes need to +be changed to either ``"body"`` or ``"status"``, and either +``"from"`` or ``"to"`` depending on if the object is a message +or presence stanza and which of the JIDs from the stanza is needed. + +Updated Code +~~~~~~~~~~~~ + +.. code-block:: python + + def handleIncomingXMPPMessage(self, event) : + message = self.addRecipientToMessage(event["body"], event["to"].jid) + user = self.backend.getUserFromJID(event["from"].jid) + self.backend.addMessageFromUser(message, user) + + def handleIncomingXMPPPresence(self, event) : + if event["to"].jid == self.componentDomain : + user = self.backend.getUserFromJID(event["from"].jid) + self.backend.addMessageFromUser(event["status"], user) + + ... + + def handleXMPPPresenceSubscription(self, subscription) : + if subscription["type"] == "subscribe" : + userJID = subscription["from"].jid + user = self.backend.getUserFromJID(userJID) + contactJID = subscription["to"] + self.xmpp.sendPresenceSubscription( + pfrom=contactJID, pto=userJID, ptype="subscribed", pnick=user) + self.sendPresenceOfContactToUser(contactJID=contactJID, userJID=userJID) + if contactJID == self.componentDomain : + self.sendAllContactSubscriptionRequestsToUser(userJID) + +`View full source `_ | +`View original code `_