diff --git a/.boring b/.boring
new file mode 100644
index 0000000..25deac6
--- /dev/null
+++ b/.boring
@@ -0,0 +1,14 @@
+# Boring file regexps:
+(^|/)_darcs($|/)
+(^|/)CVS($|/)
+(^|/)\.svn($|/)
+(^|/)\.DS_Store$
+(^|/)Thumbs\.db$
+\#
+~$
+(^|/)core(\.[0-9]+)?$
+\.(pyc|pyo|o|so|orig|bak|BAK|prof|wpu|cvsignore)$
+(^|/)build($|/)
+(^|/)dist($|/)
+(^|/)\.komodotools($|/)
+^MANIFEST$
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..88bf7a8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,680 @@
+# Copyright 2010-2011 AG Projects
+# http://ag-projects.com
+
+License
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..c9bfea3
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,9 @@
+recursive-include debian changelog compat control copyright rules
+recursive-include debian pycompat pyversions
+recursive-include debian *.init *.dirs *.default
+recursive-include debian/source format
+recursive-include resources/sounds *.wav
+recursive-include resources/sounds/moh *.wav
+prune debian/tmp
+prune debian/sylkserver-*
+include INSTALL LICENSE MANIFEST.in *.ini.sample
diff --git a/README b/README
new file mode 100644
index 0000000..af7b878
--- /dev/null
+++ b/README
@@ -0,0 +1,103 @@
+
+SylkServer
+----------
+
+Copyright (c) 2010-2011 AG Projects
+http://ag-projects.com
+
+Authors: Adrian Georgescu, Denis Bilenko, Saul Ibarra
+Home page: http://sylkserver.com
+
+
+License
+-------
+
+SylkServer is licensed under GNU General Public License version 3. A copy of
+the license is available at http://www.fsf.org/licensing/licenses/agpl-3.0.html
+
+
+Description
+-----------
+
+SylkServer is a state of the art, extensible SIP Application Server
+
+SylkServer allows creation and delivery of rich multimedia applications
+accessed by SIP User Agents. The server supports SIP signaling over TLS,
+TCP and UDP transports, RTP and MSRP media planes, has built in capabilities
+for creating ad-hoc SIP multimedia conferences with HD Audio, IM and File
+Transfer and can be easily extended with other applications by using Python
+programming language.
+
+
+Features
+--------
+
+SIP Signaling
+
+ - TLS, TCP and UDP transports
+ - INVITE and MESSAGE for sessions
+ - SUBSCRIBE/NOTIFY for conference event notifications
+
+Audio
+
+ - G722 and Speex wideband codecs
+ - G711 and GSM narrow-band codecs
+ - sRTP encryption
+ - RTP timeout
+
+Instant Messaging
+
+ - MSRP chat and SIP MESSAGE
+ - CPIM envelope
+ - Is-composing indicator
+ - History buffer
+
+Conferencing
+
+ - RTP mixer
+ - MSRP Switch, private messgaing
+ - Conference event package
+ - isfocus
+
+
+Built-in Applications
+---------------------
+
+1. Conferencing
+
+SylkServer allows SIP end-points to create ad-hoc conference rooms by
+sending INVITE to a random username at the hostname or domain where the
+server runs. Other participants can then join by sending an INVITE to the
+same SIP URI used to create the room. The INVITE and subsequent re-INVITE
+methods may contain one or more media types supported by the server. Each
+conference room mixed audio, instant messages and uploded files are
+dispatched to all participants.
+
+SIP end-points that do not support MSRP chat can join the bridge by using
+audio only, they will receive the chat messages over the SIP signaling using
+SIP MESSAGE method, which is supported by many legacy end-points. Messages
+sent to the room using SIP MESSAGE will be dispatched by either SIP MESSAGE
+or through established MSRP sessions depending on how the end-points have
+joined the room.
+
+If a participant sends a file to the SIP URI of the room, the server will
+accept it, store it for the duration of the conference and offer it to all
+participants either present at that moment, or later to those that have
+joined the conference at a later moment.
+
+
+Standards
+---------
+
+The server implements relevant features from the following standards:
+
+ * MSRP and its relay extension RFC4975, RFC4976
+ * File Transfer over RFC5547
+ * Indication of Message Composition RFC3994
+ * CPIM Message Format RFC3862
+ * Conference event package RFC4575
+ * A Framework for Conferencing with SIP RFC4353
+ * Conferencing for User Agents RFC4579
+ 5.1. INVITE: Joining a Conference Using the Conference URI - Dial-In
+ * MSRP switch draft-ietf-simple-chat-07
+
diff --git a/conference.ini.sample b/conference.ini.sample
new file mode 100644
index 0000000..f01ab88
--- /dev/null
+++ b/conference.ini.sample
@@ -0,0 +1,18 @@
+; SylkServer Conference application configuration file
+
+[Conference]
+
+; The following settings are the default used by the software, uncomment them
+; only if you want to make changes
+
+; db_uri = sqlite:///var/lib/sylkserver/conference.sqlite
+
+; Table for storing messages history
+; history_table = message_history
+
+; Playback the last messages after join a room
+; replay_history = 20
+
+; Use MESSAGE for participants that joined a conference room with audio but without MSRP chat
+; enable_sip_message = False
+
diff --git a/config.ini.sample b/config.ini.sample
new file mode 100644
index 0000000..6282a67
--- /dev/null
+++ b/config.ini.sample
@@ -0,0 +1,64 @@
+; SylkServer configuration file
+
+[Server]
+
+; The following settings are the default used by the software, uncomment
+; them only if you want to make changes
+
+; default_application = conference
+
+; trace_dir = /var/log/sylkserver
+; trace_sip = False
+; trace_msrp = False
+; trace_notifications = False
+
+; TLS can be used for encryption of SIP signaling and MSRP media. TLS is
+; disabled by default. To enabled TLS, you must have a valid X.509
+; certificate and configure it below, then set the local_tls_port in the SIP
+; section and use_tls in MSRP section
+
+; The X.509 Certificate Authorities file
+; ca_file = /etc/sylkserver/tls/ca.crt
+
+; The file containing X.509 certificate and private key in unencrypted format
+; certificate = /etc/sylkserver/tls/sylkserver.crt
+
+; verify_server = False
+
+
+[SIP]
+
+; SIP transport settings
+; IP address used for SIP signaling; empty string or any means listen on interface used
+; by the default route
+; local_ip =
+
+; Ports used for SIP transports, if not set to any value the transport will be disabled
+; local_udp_port = 5060
+; local_tcp_port = 5060
+; local_tls_port =
+
+
+[MSRP]
+
+; MSRP transport settings
+
+; use_tls = False
+
+
+[RTP]
+
+; RTP transport settings
+
+; Allowed codec list, valid values: G722, speex, PCMU, PCMA, iLBC, GSM
+; allowed_codecs = G722,speex,PCMU,PCMA
+
+; Port range used for RTP
+; port_range = 50000:50500
+
+; SRTP valid values: disabled, mandatory, optional
+; srtp_encryption = optional
+
+; RTP stream timeout, session will be disconnected after this value
+; timeout = 30
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..11fe185
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+sylkserver (1.0.0) unstable; urgency=low
+
+ * Initial release
+
+ -- Saul Ibarra Thu, 27 Jan 2011 17:43:11 +0100
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..f385732
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,19 @@
+Source: sylkserver
+Section: net
+Priority: optional
+Maintainer: Saul Ibarra
+Uploaders: Dan Pascu , Adrian Georgescu
+Build-Depends: cdbs (>= 0.4.53), debhelper (>= 7), python-all (>= 2.6), python-support
+Standards-Version: 3.9.1
+
+Package: sylkserver
+Architecture: all
+Depends: ${python:Depends}, ${misc:Depends}, python-application (>= 1.2.4), python-sipsimple (>= 0.17.0), python-sqlobject (>= 0.12.4)
+Suggests: python-mysqldb
+Description: A state of the art, extensible SIP Application Server
+ SylkServer allows creation and delivery of rich multimedia applications
+ accessed by SIP User Agents. The server supports SIP signaling over TLS,
+ TCP and UDP transports, RTP and MSRP media planes, has built in
+ capabilities for creating ad-hoc SIP multimedia conferences with HD Audio,
+ IM and File Transfer and can be easily extended with other applications by
+ using Python programming language.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..c9f3361
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,23 @@
+This work was packaged for Debian by:
+
+ Saul Ibarra on Tue Jan 25 17:21:12 CET 2011
+
+Copyright:
+
+ Copyright (C) 2010 AG Projects
+
+License:
+
+ SylkServer is licensed under GNU General Public License version 3. A copy of the
+ license is available at /usr/share/common-licenses/GPL-3 or online at:
+ http://www.gnu.org/licenses/gpl-3.0.html
+
+ The following restrictions apply:
+ * You may not alter the name of the software (SylkServer)
+ * You may not alter the Copyright and About notices
+
+The Debian packaging is:
+
+ Copyright (C) 2010 Saul Ibarra
+
+and is licensed under the GPL version 3.
diff --git a/debian/pycompat b/debian/pycompat
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/debian/pycompat
@@ -0,0 +1 @@
+2
diff --git a/debian/pyversions b/debian/pyversions
new file mode 100644
index 0000000..0c043f1
--- /dev/null
+++ b/debian/pyversions
@@ -0,0 +1 @@
+2.6-
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..e4a399b
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,15 @@
+#!/usr/bin/make -f
+
+DEB_PYTHON_SYSTEM=pysupport
+DEB_DH_INSTALLINIT_ARGS=--no-start
+
+include /usr/share/cdbs/1/class/python-distutils.mk
+include /usr/share/cdbs/1/rules/debhelper.mk
+
+clean::
+ -rm -rf build dist MANIFEST
+
+install/sylkserver::
+ install -m 0644 config.ini.sample debian/sylkserver/etc/sylkserver/config.ini
+ install -m 0644 conference.ini.sample debian/sylkserver/etc/sylkserver/conference.ini
+
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/debian/sylkserver.default b/debian/sylkserver.default
new file mode 100644
index 0000000..c489035
--- /dev/null
+++ b/debian/sylkserver.default
@@ -0,0 +1,8 @@
+#
+# SylkServer startup options
+#
+
+# Set to yes to enable SylkServer, once configured properly
+# by editing /etc/sylkserver/config.ini
+RUN_SYLKSERVER=no
+
diff --git a/debian/sylkserver.dirs b/debian/sylkserver.dirs
new file mode 100644
index 0000000..1c539de
--- /dev/null
+++ b/debian/sylkserver.dirs
@@ -0,0 +1,5 @@
+etc/sylkserver
+etc/sylkserver/tls
+usr/bin
+usr/share/sylkserver
+var/lib/sylkserver
diff --git a/debian/sylkserver.init b/debian/sylkserver.init
new file mode 100644
index 0000000..5fff4b1
--- /dev/null
+++ b/debian/sylkserver.init
@@ -0,0 +1,72 @@
+#!/bin/sh
+#
+### BEGIN INIT INFO
+# Provides: sylkserver
+# Required-Start: $syslog $network $local_fs $remote_fs $time
+# Required-Stop: $syslog $network $local_fs $remote_fs
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Start the SylkServer
+# Description: Start the SylkServer
+### END INIT INFO
+
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+
+INSTALL_DIR="/usr/bin"
+RUNTIME_DIR="/var/run/sylkserver"
+DEFAULTS="/etc/default/sylkserver"
+SERVER="$INSTALL_DIR/sylk-server"
+PID="$RUNTIME_DIR/server.pid"
+OPTIONS=""
+
+NAME="sylk-server"
+DESC="SylkServer"
+
+test -f $SERVER || exit 0
+
+. /lib/lsb/init-functions
+
+# Load startup options if available
+if [ -f $DEFAULTS ]; then
+ . $DEFAULTS || true
+fi
+
+if [ "$RUN_SYLKSERVER" != "yes" ]; then
+ echo "SylkServer not yet configured. Edit /etc/default/sylkserver first."
+ exit 0
+fi
+
+start() {
+ log_daemon_msg "Starting $DESC: $NAME "
+ start-stop-daemon --start --quiet --pidfile $PID \
+ --exec $SERVER -- $OPTIONS || log_progress_msg "already running"
+}
+
+stop () {
+ log_daemon_msg "Stopping $DESC: $NAME "
+ start-stop-daemon --stop --quiet --oknodo --pidfile $PID
+}
+
+case "$1" in
+ start)
+ start
+ log_end_msg 0
+ ;;
+ stop)
+ stop
+ log_end_msg 0
+ ;;
+ restart|force-reload)
+ stop
+ sleep 1
+ start
+ log_end_msg 0
+ ;;
+ *)
+ echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
+
diff --git a/resources/sounds/bi_0.wav b/resources/sounds/bi_0.wav
new file mode 100644
index 0000000..d1dbab3
Binary files /dev/null and b/resources/sounds/bi_0.wav differ
diff --git a/resources/sounds/bi_1.wav b/resources/sounds/bi_1.wav
new file mode 100644
index 0000000..b93fcf5
Binary files /dev/null and b/resources/sounds/bi_1.wav differ
diff --git a/resources/sounds/bi_10.wav b/resources/sounds/bi_10.wav
new file mode 100644
index 0000000..e47eeaa
Binary files /dev/null and b/resources/sounds/bi_10.wav differ
diff --git a/resources/sounds/bi_100.wav b/resources/sounds/bi_100.wav
new file mode 100644
index 0000000..4eba03d
Binary files /dev/null and b/resources/sounds/bi_100.wav differ
diff --git a/resources/sounds/bi_1000.wav b/resources/sounds/bi_1000.wav
new file mode 100644
index 0000000..4b770cd
Binary files /dev/null and b/resources/sounds/bi_1000.wav differ
diff --git a/resources/sounds/bi_10th.wav b/resources/sounds/bi_10th.wav
new file mode 100644
index 0000000..221c685
Binary files /dev/null and b/resources/sounds/bi_10th.wav differ
diff --git a/resources/sounds/bi_11.wav b/resources/sounds/bi_11.wav
new file mode 100644
index 0000000..56779d6
Binary files /dev/null and b/resources/sounds/bi_11.wav differ
diff --git a/resources/sounds/bi_11th.wav b/resources/sounds/bi_11th.wav
new file mode 100644
index 0000000..7fa5230
Binary files /dev/null and b/resources/sounds/bi_11th.wav differ
diff --git a/resources/sounds/bi_12.wav b/resources/sounds/bi_12.wav
new file mode 100644
index 0000000..8bc458b
Binary files /dev/null and b/resources/sounds/bi_12.wav differ
diff --git a/resources/sounds/bi_12th.wav b/resources/sounds/bi_12th.wav
new file mode 100644
index 0000000..e540b5a
Binary files /dev/null and b/resources/sounds/bi_12th.wav differ
diff --git a/resources/sounds/bi_13.wav b/resources/sounds/bi_13.wav
new file mode 100644
index 0000000..554d168
Binary files /dev/null and b/resources/sounds/bi_13.wav differ
diff --git a/resources/sounds/bi_13th.wav b/resources/sounds/bi_13th.wav
new file mode 100644
index 0000000..84d7718
Binary files /dev/null and b/resources/sounds/bi_13th.wav differ
diff --git a/resources/sounds/bi_14.wav b/resources/sounds/bi_14.wav
new file mode 100644
index 0000000..fc661ac
Binary files /dev/null and b/resources/sounds/bi_14.wav differ
diff --git a/resources/sounds/bi_14th.wav b/resources/sounds/bi_14th.wav
new file mode 100644
index 0000000..f402c8f
Binary files /dev/null and b/resources/sounds/bi_14th.wav differ
diff --git a/resources/sounds/bi_15.wav b/resources/sounds/bi_15.wav
new file mode 100644
index 0000000..71e66fa
Binary files /dev/null and b/resources/sounds/bi_15.wav differ
diff --git a/resources/sounds/bi_15th.wav b/resources/sounds/bi_15th.wav
new file mode 100644
index 0000000..a94efd0
Binary files /dev/null and b/resources/sounds/bi_15th.wav differ
diff --git a/resources/sounds/bi_16.wav b/resources/sounds/bi_16.wav
new file mode 100644
index 0000000..1e2d438
Binary files /dev/null and b/resources/sounds/bi_16.wav differ
diff --git a/resources/sounds/bi_16th.wav b/resources/sounds/bi_16th.wav
new file mode 100644
index 0000000..2c91d9b
Binary files /dev/null and b/resources/sounds/bi_16th.wav differ
diff --git a/resources/sounds/bi_17.wav b/resources/sounds/bi_17.wav
new file mode 100644
index 0000000..6516848
Binary files /dev/null and b/resources/sounds/bi_17.wav differ
diff --git a/resources/sounds/bi_17th.wav b/resources/sounds/bi_17th.wav
new file mode 100644
index 0000000..deb404b
Binary files /dev/null and b/resources/sounds/bi_17th.wav differ
diff --git a/resources/sounds/bi_18.wav b/resources/sounds/bi_18.wav
new file mode 100644
index 0000000..15c819d
Binary files /dev/null and b/resources/sounds/bi_18.wav differ
diff --git a/resources/sounds/bi_18th.wav b/resources/sounds/bi_18th.wav
new file mode 100644
index 0000000..9e20df4
Binary files /dev/null and b/resources/sounds/bi_18th.wav differ
diff --git a/resources/sounds/bi_19.wav b/resources/sounds/bi_19.wav
new file mode 100644
index 0000000..4769bfd
Binary files /dev/null and b/resources/sounds/bi_19.wav differ
diff --git a/resources/sounds/bi_19th.wav b/resources/sounds/bi_19th.wav
new file mode 100644
index 0000000..5e7d5b0
Binary files /dev/null and b/resources/sounds/bi_19th.wav differ
diff --git a/resources/sounds/bi_1f.wav b/resources/sounds/bi_1f.wav
new file mode 100644
index 0000000..b93fcf5
Binary files /dev/null and b/resources/sounds/bi_1f.wav differ
diff --git a/resources/sounds/bi_1n.wav b/resources/sounds/bi_1n.wav
new file mode 100644
index 0000000..b93fcf5
Binary files /dev/null and b/resources/sounds/bi_1n.wav differ
diff --git a/resources/sounds/bi_1th.wav b/resources/sounds/bi_1th.wav
new file mode 100644
index 0000000..ab27ce0
Binary files /dev/null and b/resources/sounds/bi_1th.wav differ
diff --git a/resources/sounds/bi_2.wav b/resources/sounds/bi_2.wav
new file mode 100644
index 0000000..dc5002d
Binary files /dev/null and b/resources/sounds/bi_2.wav differ
diff --git a/resources/sounds/bi_20.wav b/resources/sounds/bi_20.wav
new file mode 100644
index 0000000..3711da5
Binary files /dev/null and b/resources/sounds/bi_20.wav differ
diff --git a/resources/sounds/bi_2000.wav b/resources/sounds/bi_2000.wav
new file mode 100644
index 0000000..f6f4fe8
Binary files /dev/null and b/resources/sounds/bi_2000.wav differ
diff --git a/resources/sounds/bi_20th.wav b/resources/sounds/bi_20th.wav
new file mode 100644
index 0000000..c364b69
Binary files /dev/null and b/resources/sounds/bi_20th.wav differ
diff --git a/resources/sounds/bi_21.wav b/resources/sounds/bi_21.wav
new file mode 100644
index 0000000..8f8c422
Binary files /dev/null and b/resources/sounds/bi_21.wav differ
diff --git a/resources/sounds/bi_21th.wav b/resources/sounds/bi_21th.wav
new file mode 100644
index 0000000..363724a
Binary files /dev/null and b/resources/sounds/bi_21th.wav differ
diff --git a/resources/sounds/bi_22.wav b/resources/sounds/bi_22.wav
new file mode 100644
index 0000000..2457973
Binary files /dev/null and b/resources/sounds/bi_22.wav differ
diff --git a/resources/sounds/bi_22th.wav b/resources/sounds/bi_22th.wav
new file mode 100644
index 0000000..c1df552
Binary files /dev/null and b/resources/sounds/bi_22th.wav differ
diff --git a/resources/sounds/bi_23.wav b/resources/sounds/bi_23.wav
new file mode 100644
index 0000000..d17bc31
Binary files /dev/null and b/resources/sounds/bi_23.wav differ
diff --git a/resources/sounds/bi_23th.wav b/resources/sounds/bi_23th.wav
new file mode 100644
index 0000000..5b32266
Binary files /dev/null and b/resources/sounds/bi_23th.wav differ
diff --git a/resources/sounds/bi_24.wav b/resources/sounds/bi_24.wav
new file mode 100644
index 0000000..b56e5fa
Binary files /dev/null and b/resources/sounds/bi_24.wav differ
diff --git a/resources/sounds/bi_24th.wav b/resources/sounds/bi_24th.wav
new file mode 100644
index 0000000..81b5230
Binary files /dev/null and b/resources/sounds/bi_24th.wav differ
diff --git a/resources/sounds/bi_25th.wav b/resources/sounds/bi_25th.wav
new file mode 100644
index 0000000..f00211e
Binary files /dev/null and b/resources/sounds/bi_25th.wav differ
diff --git a/resources/sounds/bi_26th.wav b/resources/sounds/bi_26th.wav
new file mode 100644
index 0000000..f5f80e0
Binary files /dev/null and b/resources/sounds/bi_26th.wav differ
diff --git a/resources/sounds/bi_27th.wav b/resources/sounds/bi_27th.wav
new file mode 100644
index 0000000..4f47d82
Binary files /dev/null and b/resources/sounds/bi_27th.wav differ
diff --git a/resources/sounds/bi_28th.wav b/resources/sounds/bi_28th.wav
new file mode 100644
index 0000000..7b0e9e6
Binary files /dev/null and b/resources/sounds/bi_28th.wav differ
diff --git a/resources/sounds/bi_29th.wav b/resources/sounds/bi_29th.wav
new file mode 100644
index 0000000..1f45b93
Binary files /dev/null and b/resources/sounds/bi_29th.wav differ
diff --git a/resources/sounds/bi_2th.wav b/resources/sounds/bi_2th.wav
new file mode 100644
index 0000000..88f72af
Binary files /dev/null and b/resources/sounds/bi_2th.wav differ
diff --git a/resources/sounds/bi_3.wav b/resources/sounds/bi_3.wav
new file mode 100644
index 0000000..96f1c1a
Binary files /dev/null and b/resources/sounds/bi_3.wav differ
diff --git a/resources/sounds/bi_30.wav b/resources/sounds/bi_30.wav
new file mode 100644
index 0000000..eb57682
Binary files /dev/null and b/resources/sounds/bi_30.wav differ
diff --git a/resources/sounds/bi_30th.wav b/resources/sounds/bi_30th.wav
new file mode 100644
index 0000000..0bee7a7
Binary files /dev/null and b/resources/sounds/bi_30th.wav differ
diff --git a/resources/sounds/bi_31th.wav b/resources/sounds/bi_31th.wav
new file mode 100644
index 0000000..1edc0ea
Binary files /dev/null and b/resources/sounds/bi_31th.wav differ
diff --git a/resources/sounds/bi_3th.wav b/resources/sounds/bi_3th.wav
new file mode 100644
index 0000000..5bde456
Binary files /dev/null and b/resources/sounds/bi_3th.wav differ
diff --git a/resources/sounds/bi_4.wav b/resources/sounds/bi_4.wav
new file mode 100644
index 0000000..f074642
Binary files /dev/null and b/resources/sounds/bi_4.wav differ
diff --git a/resources/sounds/bi_40.wav b/resources/sounds/bi_40.wav
new file mode 100644
index 0000000..8e794ad
Binary files /dev/null and b/resources/sounds/bi_40.wav differ
diff --git a/resources/sounds/bi_4th.wav b/resources/sounds/bi_4th.wav
new file mode 100644
index 0000000..2fb2d2c
Binary files /dev/null and b/resources/sounds/bi_4th.wav differ
diff --git a/resources/sounds/bi_5.wav b/resources/sounds/bi_5.wav
new file mode 100644
index 0000000..14eb7d6
Binary files /dev/null and b/resources/sounds/bi_5.wav differ
diff --git a/resources/sounds/bi_50.wav b/resources/sounds/bi_50.wav
new file mode 100644
index 0000000..ec6fa76
Binary files /dev/null and b/resources/sounds/bi_50.wav differ
diff --git a/resources/sounds/bi_5th.wav b/resources/sounds/bi_5th.wav
new file mode 100644
index 0000000..71e1816
Binary files /dev/null and b/resources/sounds/bi_5th.wav differ
diff --git a/resources/sounds/bi_6.wav b/resources/sounds/bi_6.wav
new file mode 100644
index 0000000..ce319e8
Binary files /dev/null and b/resources/sounds/bi_6.wav differ
diff --git a/resources/sounds/bi_60.wav b/resources/sounds/bi_60.wav
new file mode 100644
index 0000000..a9dbe85
Binary files /dev/null and b/resources/sounds/bi_60.wav differ
diff --git a/resources/sounds/bi_6th.wav b/resources/sounds/bi_6th.wav
new file mode 100644
index 0000000..b8760c2
Binary files /dev/null and b/resources/sounds/bi_6th.wav differ
diff --git a/resources/sounds/bi_7.wav b/resources/sounds/bi_7.wav
new file mode 100644
index 0000000..4cf0795
Binary files /dev/null and b/resources/sounds/bi_7.wav differ
diff --git a/resources/sounds/bi_70.wav b/resources/sounds/bi_70.wav
new file mode 100644
index 0000000..3bd05c4
Binary files /dev/null and b/resources/sounds/bi_70.wav differ
diff --git a/resources/sounds/bi_7th.wav b/resources/sounds/bi_7th.wav
new file mode 100644
index 0000000..65524a9
Binary files /dev/null and b/resources/sounds/bi_7th.wav differ
diff --git a/resources/sounds/bi_8.wav b/resources/sounds/bi_8.wav
new file mode 100644
index 0000000..0ea1a2d
Binary files /dev/null and b/resources/sounds/bi_8.wav differ
diff --git a/resources/sounds/bi_80.wav b/resources/sounds/bi_80.wav
new file mode 100644
index 0000000..e7a971d
Binary files /dev/null and b/resources/sounds/bi_80.wav differ
diff --git a/resources/sounds/bi_8th.wav b/resources/sounds/bi_8th.wav
new file mode 100644
index 0000000..1223afb
Binary files /dev/null and b/resources/sounds/bi_8th.wav differ
diff --git a/resources/sounds/bi_9.wav b/resources/sounds/bi_9.wav
new file mode 100644
index 0000000..002efbd
Binary files /dev/null and b/resources/sounds/bi_9.wav differ
diff --git a/resources/sounds/bi_90.wav b/resources/sounds/bi_90.wav
new file mode 100644
index 0000000..70c7fdf
Binary files /dev/null and b/resources/sounds/bi_90.wav differ
diff --git a/resources/sounds/bi_9th.wav b/resources/sounds/bi_9th.wav
new file mode 100644
index 0000000..1612c61
Binary files /dev/null and b/resources/sounds/bi_9th.wav differ
diff --git a/resources/sounds/co_enter_pin.wav b/resources/sounds/co_enter_pin.wav
new file mode 100644
index 0000000..79468d5
Binary files /dev/null and b/resources/sounds/co_enter_pin.wav differ
diff --git a/resources/sounds/co_join.wav b/resources/sounds/co_join.wav
new file mode 100644
index 0000000..ddbc7e3
Binary files /dev/null and b/resources/sounds/co_join.wav differ
diff --git a/resources/sounds/co_more_participants.wav b/resources/sounds/co_more_participants.wav
new file mode 100644
index 0000000..148b65b
Binary files /dev/null and b/resources/sounds/co_more_participants.wav differ
diff --git a/resources/sounds/co_only_one.wav b/resources/sounds/co_only_one.wav
new file mode 100644
index 0000000..116dba1
Binary files /dev/null and b/resources/sounds/co_only_one.wav differ
diff --git a/resources/sounds/co_right_pin.wav b/resources/sounds/co_right_pin.wav
new file mode 100644
index 0000000..05decdb
Binary files /dev/null and b/resources/sounds/co_right_pin.wav differ
diff --git a/resources/sounds/co_say_name.wav b/resources/sounds/co_say_name.wav
new file mode 100644
index 0000000..35d0e56
Binary files /dev/null and b/resources/sounds/co_say_name.wav differ
diff --git a/resources/sounds/co_there_are.wav b/resources/sounds/co_there_are.wav
new file mode 100644
index 0000000..0dd48c5
Binary files /dev/null and b/resources/sounds/co_there_are.wav differ
diff --git a/resources/sounds/co_there_is.wav b/resources/sounds/co_there_is.wav
new file mode 100644
index 0000000..8008084
Binary files /dev/null and b/resources/sounds/co_there_is.wav differ
diff --git a/resources/sounds/co_welcome_conference.wav b/resources/sounds/co_welcome_conference.wav
new file mode 100644
index 0000000..d2fc042
Binary files /dev/null and b/resources/sounds/co_welcome_conference.wav differ
diff --git a/resources/sounds/co_wrong_pin.wav b/resources/sounds/co_wrong_pin.wav
new file mode 100644
index 0000000..4aa1b42
Binary files /dev/null and b/resources/sounds/co_wrong_pin.wav differ
diff --git a/resources/sounds/moh/Cold_Day_vbr.wav b/resources/sounds/moh/Cold_Day_vbr.wav
new file mode 100644
index 0000000..e23d451
Binary files /dev/null and b/resources/sounds/moh/Cold_Day_vbr.wav differ
diff --git a/resources/sounds/moh/Cool_Grass_vbr.wav b/resources/sounds/moh/Cool_Grass_vbr.wav
new file mode 100644
index 0000000..2cffe68
Binary files /dev/null and b/resources/sounds/moh/Cool_Grass_vbr.wav differ
diff --git a/resources/sounds/moh/Listening_to_the_Birds_vbr.wav b/resources/sounds/moh/Listening_to_the_Birds_vbr.wav
new file mode 100644
index 0000000..44efa97
Binary files /dev/null and b/resources/sounds/moh/Listening_to_the_Birds_vbr.wav differ
diff --git a/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav b/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav
new file mode 100644
index 0000000..6658c7d
Binary files /dev/null and b/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav differ
diff --git a/resources/sounds/moh/Manolo Camp - morningcoffee.wav b/resources/sounds/moh/Manolo Camp - morningcoffee.wav
new file mode 100644
index 0000000..50362f3
Binary files /dev/null and b/resources/sounds/moh/Manolo Camp - morningcoffee.wav differ
diff --git a/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav b/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav
new file mode 100644
index 0000000..8cee19b
Binary files /dev/null and b/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav differ
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..8b99e9f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
+#
+
+import glob
+import os
+
+from distutils.core import setup
+
+from sylk import __version__
+
+
+def find_packages(toplevel):
+ return [directory.replace(os.path.sep, '.') for directory, subdirs, files in os.walk(toplevel) if '__init__.py' in files]
+
+setup(name = "sylkserver",
+ version = __version__,
+ author = "AG Projects",
+ author_email = "support@ag-projects.com",
+ url = "http://sylkserver.com",
+ description = "SylkServer - A state of the art, extensible SIP Application Server",
+ classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Service Providers",
+ "License :: GNU Lesser General Public License 3",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python"
+ ],
+ packages = find_packages('sylk'),
+ scripts = ['sylk-server'],
+ data_files = [('/etc/sylkserver/tls', []),
+ ('/var/lib/sylkserver', []),
+ ('share/sylkserver/sounds', glob.glob(os.path.join('resources', 'sounds', '*.wav'))),
+ ('share/sylkserver/sounds/moh', glob.glob(os.path.join('resources', 'sounds', 'moh','*.wav')))]
+ )
+
diff --git a/sylk-server b/sylk-server
new file mode 100755
index 0000000..91e8ef1
--- /dev/null
+++ b/sylk-server
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
+#
+
+import os
+import signal
+import sys
+
+from optparse import OptionParser
+
+from application import log
+from application.process import process, ProcessError
+
+import sylk
+
+
+DEBUG = False
+
+def stop_server(*args):
+ from sylk.server import SylkServer
+ log.msg('Stopping SylkServer...')
+ server = SylkServer()
+ server.stop()
+
+def main():
+ name = 'sylk-server'
+ fullname = 'SylkServer'
+ runtime_directory = '/var/run/sylkserver'
+ system_config_directory = '/etc/sylkserver'
+ default_pid = os.path.join(runtime_directory, 'server.pid')
+ default_config = os.path.join(system_config_directory , sylk.configuration_filename)
+
+ parser = OptionParser(version='%%prog %s' % sylk.__version__)
+ parser.add_option('--no-fork', action='store_false', dest='fork', default=1,
+ help='run the process in the foreground (for debugging)')
+ parser.add_option('--pid', dest='pid_file',
+ help='pid file ("%s")' % default_pid, metavar='File')
+ parser.add_option('--config-file', dest='config_file', default=default_config,
+ help='path to configuration file to read ("%s")' % default_config,
+ metavar='File')
+ (options, args) = parser.parse_args()
+
+ try:
+ sylk.dependencies.check()
+ except Exception, e:
+ log.fatal(str(e))
+ sys.exit(1)
+
+ path, configuration_filename = os.path.split(options.config_file)
+ if path:
+ system_config_directory = path
+
+ process.system_config_directory = system_config_directory
+ sylk.configuration_filename = process.config_file(configuration_filename)
+
+ # when run in foreground, do not require root access because of /var/run/sylkserver
+ if not options.fork:
+ process._runtime_directory = None
+ else:
+ try:
+ process.runtime_directory = runtime_directory
+ except ProcessError, e:
+ log.fatal("Cannot start %s: %s" % (fullname, e))
+ sys.exit(1)
+
+ pid_file = options.pid_file or default_pid
+
+ if options.fork:
+ try:
+ process.daemonize(pid_file)
+ except ProcessError, e:
+ log.fatal("Cannot start %s: %s" % (fullname, e))
+ sys.exit(1)
+ log.start_syslog(name)
+
+ if sylk.configuration_filename:
+ log.msg("Starting %s %s, config=%s" % (fullname, sylk.__version__, sylk.configuration_filename))
+ else:
+ log.fatal('Config file not found')
+ sys.exit(1)
+
+ try:
+ if not options.fork and DEBUG:
+ from application.debug.memory import memory_dump
+ from sylk.server import SylkServer
+ server = SylkServer()
+ except Exception, e:
+ log.fatal("failed to create %s: %s" % (fullname, e))
+ log.err()
+ sys.exit(1)
+
+ process.signals.add_handler(signal.SIGTERM, stop_server)
+ process.signals.add_handler(signal.SIGINT, stop_server)
+
+ try:
+ server.start()
+ signal.pause()
+ server.stop_event.wait()
+ log.msg("%s stopped" % fullname)
+ except Exception, e:
+ log.fatal("failed to run %s: %s" % (fullname, e))
+ log.err()
+ sys.exit(1)
+
+ if not options.fork and DEBUG:
+ memory_dump()
+
+
+if __name__ == "__main__":
+ main()
+
+
diff --git a/sylk/__init__.py b/sylk/__init__.py
new file mode 100644
index 0000000..065cf99
--- /dev/null
+++ b/sylk/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
+
+"""SylkServer"""
+
+__version__ = '1.0.0'
+
+configuration_filename = "config.ini"
+
+
+package_requirements = {'python-application': '1.2.4',
+ 'python-sipsimple': '0.17.0'}
+
+try:
+ from application.dependency import ApplicationDependencies
+except:
+ class DependencyError(Exception): pass
+ class ApplicationDependencies(object):
+ def __init__(self, *args, **kwargs):
+ pass
+ def check(self):
+ raise DependencyError("need python-application version %s or higher but it's not installed" % package_requirements['python-application'])
+
+dependencies = ApplicationDependencies(**package_requirements)
+
+
diff --git a/sylk/applications/__init__.py b/sylk/applications/__init__.py
new file mode 100644
index 0000000..b56d729
--- /dev/null
+++ b/sylk/applications/__init__.py
@@ -0,0 +1,126 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
+#
+
+__all__ = ['ISylkApplication', 'ApplicationRegistry', 'sylk_application', 'IncomingRequestHandler']
+
+import os
+
+from application import log
+from application.notification import IObserver, NotificationCenter
+from application.python.util import Null, Singleton
+from sipsimple.threading import run_in_twisted_thread
+from zope.interface import Attribute, Interface, implements
+
+from sylk.configuration import ServerConfig
+
+
+class ISylkApplication(Interface):
+ """
+ Interface defining attributes and methods any application must
+ implement.
+
+ Each application must be a Singleton and has to be decorated with
+ the @sylk_application decorator.
+ """
+
+ __appname__ = Attribute("Application name")
+
+ def incoming_session(self, session):
+ pass
+
+ def incoming_subscription(self, subscribe_request, data):
+ pass
+
+ def incoming_sip_message(self, message_request, data):
+ pass
+
+
+class ApplicationRegistry(object):
+ __metaclass__ = Singleton
+
+ def __init__(self):
+ self.applications = []
+
+ def __iter__(self):
+ return iter(self.applications)
+
+ def add(self, app):
+ if app not in self.applications:
+ self.applications.append(app)
+
+
+def sylk_application(cls):
+ """Class decorator for adding applications to the ApplicationRegistry"""
+ ApplicationRegistry().add(cls())
+ return cls
+
+
+def load_applications():
+ toplevel = os.path.dirname(__file__)
+ app_list = ['sylk.applications.%s' % item for item in os.listdir(toplevel) if os.path.isdir(os.path.join(toplevel, item)) and '__init__.py' in os.listdir(os.path.join(toplevel, item))]
+ map(__import__, app_list)
+
+
+class IncomingRequestHandler(object):
+ """
+ Handle incoming requests and match them to applications.
+ """
+ __metaclass__ = Singleton
+ implements(IObserver)
+
+ # TODO: implement a 'find_application' function which will get the appropriate application
+ # as defined in the configuration
+ # TODO: apply ACLs (before or after?)
+ def __init__(self):
+ load_applications()
+ log.msg('Loaded applications: %s' % ','.join([app.__appname__ for app in ApplicationRegistry()]))
+
+ def start(self):
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, name='SIPSessionNewIncoming')
+ notification_center.add_observer(self, name='SIPIncomingSubscriptionGotSubscribe')
+ notification_center.add_observer(self, name='SIPIncomingRequestGotRequest')
+
+ def stop(self):
+ notification_center = NotificationCenter()
+ notification_center.remove_observer(self, name='SIPSessionNewIncoming')
+ notification_center.remove_observer(self, name='SIPIncomingSubscriptionGotSubscribe')
+ notification_center.remove_observer(self, name='SIPIncomingRequestGotRequest')
+
+ @run_in_twisted_thread
+ def handle_notification(self, notification):
+ handler = getattr(self, '_NH_%s' % notification.name, Null)
+ handler(notification)
+
+ def _NH_SIPSessionNewIncoming(self, notification):
+ session = notification.sender
+ try:
+ app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next()
+ except StopIteration:
+ pass
+ else:
+ app.incoming_session(session)
+
+ def _NH_SIPIncomingSubscriptionGotSubscribe(self, notification):
+ subscribe_request = notification.sender
+ try:
+ app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next()
+ except StopIteration:
+ pass
+ else:
+ app.incoming_subscription(subscribe_request, notification.data)
+
+ def _NH_SIPIncomingRequestGotRequest(self, notification):
+ request = notification.sender
+ if notification.data.method != 'MESSAGE':
+ request.answer(405)
+ return
+ try:
+ app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next()
+ except StopIteration:
+ pass
+ else:
+ app.incoming_sip_message(request, notification.data)
+
+
+
diff --git a/sylk/applications/conference/__init__.py b/sylk/applications/conference/__init__.py
new file mode 100644
index 0000000..dc99035
--- /dev/null
+++ b/sylk/applications/conference/__init__.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
+#
+
+from application import log
+from application.notification import IObserver, NotificationCenter
+from application.python.util import Null, Singleton
+from twisted.internet import reactor
+from zope.interface import implements
+
+from sylk.applications import ISylkApplication, sylk_application
+from sylk.applications.conference.configuration import ConferenceConfig
+from sylk.applications.conference.room import Room
+
+# Initialize database
+from sylk.applications.conference import database
+
+
+@sylk_application
+class ConferenceApplication(object):
+ __metaclass__ = Singleton
+ implements(ISylkApplication, IObserver)
+
+ __appname__ = 'conference'
+
+ def __init__(self):
+ self.rooms = set()
+ self.pending_sessions = []
+
+ def incoming_session(self, session):
+ log.msg('New incoming session from %s' % session.remote_identity.uri)
+ audio_streams = [stream for stream in session.proposed_streams if stream.type=='audio']
+ chat_streams = [stream for stream in session.proposed_streams if stream.type=='chat']
+ if not audio_streams and not chat_streams:
+ session.reject(488)
+ return
+ self.pending_sessions.append(session)
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, sender=session)
+ if audio_streams:
+ session.send_ring_indication()
+ streams = [streams[0] for streams in (audio_streams, chat_streams) if streams]
+ reactor.callLater(4 if audio_streams else 0, self.accept_session, session, streams)
+
+ def incoming_subscription(self, subscribe_request, data):
+ to_header = data.headers.get('To', Null)
+ if to_header is Null:
+ subscribe_request.reject(400)
+ return
+ room = Room.get_room(data.request_uri)
+ if not room.started:
+ room = Room.get_room(to_header.uri)
+ if not room.started:
+ subscribe_request.reject(480)
+ return
+ room.handle_incoming_subscription(subscribe_request, data)
+
+ def incoming_sip_message(self, message_request, data):
+ if not ConferenceConfig.enable_sip_message:
+ return
+ room = Room.get_room(data.request_uri)
+ if not room.started:
+ message_request.answer(480)
+ return
+ room.handle_incoming_sip_message(message_request, data)
+
+ def accept_session(self, session, streams):
+ if session in self.pending_sessions:
+ session.accept(streams, is_focus=True)
+
+ def handle_notification(self, notification):
+ handler = getattr(self, '_NH_%s' % notification.name, Null)
+ handler(notification)
+
+ def _NH_SIPSessionDidStart(self, notification):
+ session = notification.sender
+ self.pending_sessions.remove(session)
+ room = Room.get_room(session.local_identity.uri)
+ room.start()
+ room.add_session(session)
+ self.rooms.add(room)
+
+ def _NH_SIPSessionDidEnd(self, notification):
+ session = notification.sender
+ log.msg('Session from %s ended' % session.remote_identity.uri)
+ notification_center = NotificationCenter()
+ notification_center.remove_observer(self, sender=session)
+ room = Room.get_room(session.local_identity.uri)
+ if session in room.sessions:
+ # We could get this notifiction even if we didn't get SIPSessionDidStart
+ room.remove_session(session)
+ if room.empty:
+ room.stop()
+ try:
+ self.rooms.remove(room)
+ except KeyError:
+ pass
+
+ def _NH_SIPSessionDidFail(self, notification):
+ session = notification.sender
+ self.pending_sessions.remove(session)
+ log.msg('Session from %s failed' % session.remote_identity.uri)
+
+
diff --git a/sylk/applications/conference/configuration.py b/sylk/applications/conference/configuration.py
new file mode 100644
index 0000000..bfb5cb9
--- /dev/null
+++ b/sylk/applications/conference/configuration.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+from application.configuration import ConfigSection, ConfigSetting
+
+
+class ConferenceConfig(ConfigSection):
+ __cfgfile__ = 'conference.ini'
+ __section__ = 'Conference'
+
+ db_uri = ConfigSetting(type=str, value='sqlite:///var/lib/sylkserver/conference.sqlite')
+ history_table = ConfigSetting(type=str, value='message_history')
+ enable_sip_message = False
+ replay_history = 20
+
+
diff --git a/sylk/applications/conference/database.py b/sylk/applications/conference/database.py
new file mode 100644
index 0000000..efa441f
--- /dev/null
+++ b/sylk/applications/conference/database.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+__all__ = ['async_save_message', 'get_last_messages']
+
+import datetime
+
+from application.python.util import Null
+from eventlet.twistedutil import block_on
+from sqlobject import SQLObject, DateTimeCol, UnicodeCol
+from twisted.internet.threads import deferToThread
+
+from sylk.applications.conference.configuration import ConferenceConfig
+from sylk.database import Database
+
+
+db = Database(ConferenceConfig.db_uri)
+
+
+class MessageHistory(SQLObject):
+ class sqlmeta:
+ table = ConferenceConfig.history_table
+ _connection = db.connection
+ date = DateTimeCol()
+ room_uri = UnicodeCol()
+ sip_from = UnicodeCol()
+ cpim_body = UnicodeCol(sqlType='LONGTEXT')
+ cpim_content_type = UnicodeCol()
+ cpim_sender = UnicodeCol()
+ cpim_recipient = UnicodeCol()
+ cpim_timestamp = DateTimeCol()
+
+db.create_table(MessageHistory)
+
+
+def _save_message(sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp):
+ return MessageHistory(date = datetime.datetime.utcnow(),
+ room_uri = room_uri,
+ sip_from = sip_from,
+ cpim_body = cpim_body,
+ cpim_content_type = cpim_content_type,
+ cpim_sender = cpim_sender,
+ cpim_recipient = cpim_recipient,
+ cpim_timestamp = cpim_timestamp)
+
+def async_save_message(sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp):
+ if db.connection is not Null:
+ return deferToThread(_save_message, sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp)
+
+def _get_last_messages(room_uri, count):
+ return list(MessageHistory.selectBy(room_uri=room_uri)[-count:])
+
+def get_last_messages(room_uri, count):
+ if db.connection is not Null and count > 0:
+ return block_on(deferToThread(_get_last_messages, room_uri, count))
+ else:
+ return []
+
+
diff --git a/sylk/applications/conference/room.py b/sylk/applications/conference/room.py
new file mode 100644
index 0000000..c4e3f94
--- /dev/null
+++ b/sylk/applications/conference/room.py
@@ -0,0 +1,652 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+import random
+
+from datetime import datetime
+from glob import glob
+from itertools import cycle
+from time import mktime
+
+from application import log
+from application.notification import IObserver, NotificationCenter
+from application.python.util import Null, Singleton
+from eventlet import coros, proc
+from sipsimple.application import SIPApplication
+from sipsimple.audio import WavePlayer, WavePlayerError
+from sipsimple.conference import AudioConference
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.core import FromHeader, ToHeader, RouteHeader, SIPURI, Message, SIPCoreInvalidStateError
+from sipsimple.lookup import DNSLookup, DNSLookupError
+from sipsimple.payloads.conference import Conference, ConferenceDescription, ConferenceState, Endpoint, EndpointStatus, HostInfo, JoiningInfo, Media, User, Users, WebPage
+from sipsimple.payloads.iscomposing import IsComposingMessage, State, LastActive, Refresh, ContentType
+from sipsimple.streams.applications.chat import CPIMIdentity, CPIMMessage, CPIMParserError
+from sipsimple.streams.msrp import ChatStreamError
+from sipsimple.threading import run_in_twisted_thread
+from sipsimple.threading.green import run_in_green_thread
+from sipsimple.util import Timestamp
+from twisted.internet import reactor
+from zope.interface import implements
+
+from sylk.applications.conference import database
+from sylk.applications.conference.configuration import ConferenceConfig
+from sylk.configuration.datatypes import ResourcePath
+
+
+def format_identity(identity, cpim_format=False):
+ uri = identity.uri
+ if identity.display_name:
+ return '%s ' % (identity.display_name, uri.user, uri.host)
+ elif cpim_format:
+ return '' % (uri.user, uri.host)
+ else:
+ return 'sip:%s@%s' % (uri.user, uri.host)
+
+def format_stream_types(streams):
+ if not streams:
+ return ''
+ if len(streams) == 1:
+ txt = 'with %s' % streams[0].type
+ else:
+ txt = 'with %s' % ','.join(stream.type for stream in streams[:-1])
+ txt += ' and %s' % streams[-1:][0].type
+ return txt
+
+def format_conference_stream_type(stream):
+ if stream.type == 'chat':
+ return 'message'
+ return stream.type
+
+def format_identity_with_stream_types(identity, streams):
+ return '%s %s' % (format_identity(identity), format_stream_types(streams))
+
+def format_session_duration(session):
+ if session.start_time:
+ duration = session.end_time - session.start_time
+ seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1
+ minutes, seconds = seconds / 60, seconds % 60
+ hours, minutes = minutes / 60, minutes % 60
+ hours += duration.days*24
+ if not minutes and not hours:
+ duration_text = '%d seconds' % seconds
+ elif not hours:
+ duration_text = '%02d:%02d' % (minutes, seconds)
+ else:
+ duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds)
+ else:
+ duration_text = '0s'
+ return duration_text
+
+def chunks(text, size):
+ for i in xrange(0, len(text), size):
+ yield text[i:i+size]
+
+
+class SIPMessage(object):
+ def __init__(self, sender, recipient, content_type, body):
+ self.sender = sender
+ self.recipient = recipient
+ self.content_type = content_type
+ self.body = body
+ self.timestamp = None
+
+
+class Room(object):
+ """
+ Object representing a conference room, it will handle the message dispatching
+ among all the participants.
+ """
+ __metaclass__ = Singleton
+ implements(IObserver)
+
+ def __init__(self, uri):
+ self.uri = uri
+ self.identity = CPIMIdentity.parse('<%s>' % self.uri)
+ self.sessions = []
+ self.sessions_with_proposals = []
+ self.subscriptions = []
+ self.state = 'stopped'
+ self.incoming_message_queue = coros.queue()
+ self.message_dispatcher = None
+ self.audio_conference = None
+ self.moh_player = None
+ self.conference_info_payload = None
+
+ @classmethod
+ def get_room(cls, uri):
+ room_uri = '%s@%s' % (uri.user, uri.host)
+ room = cls(room_uri)
+ return room
+
+ @property
+ def empty(self):
+ return len(self.sessions) == 0
+
+ @property
+ def started(self):
+ return self.state == 'started'
+
+ def start(self):
+ if self.state != 'stopped':
+ return
+ self.message_dispatcher = proc.spawn(self._message_dispatcher)
+ self.audio_conference = AudioConference()
+ self.audio_conference.hold()
+ self.moh_player = MoHPlayer(self.audio_conference)
+ self.moh_player.initialize()
+ self.state = 'started'
+
+ def stop(self):
+ if self.state != 'started':
+ return
+ self.state = 'stopped'
+ self.message_dispatcher.kill(proc.ProcExit)
+ self.moh_player = None
+ self.audio_conference = None
+
+ def _message_dispatcher(self):
+ """Read from self.incoming_message_queue and dispatch the messages to other participants"""
+ while True:
+ session, message_type, data = self.incoming_message_queue.wait()
+ if message_type == 'message':
+ if data.timestamp is not None and isinstance(data.timestamp, Timestamp):
+ timestamp = datetime.fromtimestamp(mktime(data.timestamp.timetuple()))
+ else:
+ timestamp = datetime.now()
+ if data.sender.uri != session.remote_identity.uri:
+ return
+ recipient = data.recipients[0]
+ database.async_save_message(format_identity(session.remote_identity, True), self.uri, data.body, data.content_type, unicode(data.sender), unicode(recipient), timestamp)
+ if recipient.uri == self.identity.uri:
+ self.dispatch_message(session, data)
+ else:
+ self.dispatch_private_message(session, data)
+ elif message_type == 'sip_message':
+ database.async_save_message(format_identity(session.remote_identity, True), self.uri, data.body, data.content_type, unicode(data.sender), data.recipient, data.timestamp)
+ self.dispatch_message(session, data)
+ elif message_type == 'composing_indication':
+ if data.sender.uri != session.remote_identity.uri:
+ return
+ recipient = data.recipients[0]
+ if recipient.uri == self.identity.uri:
+ self.dispatch_iscomposing(session, data)
+ else:
+ self.dispatch_private_iscomposing(session, data)
+
+ def dispatch_message(self, session, message):
+ for s in (s for s in self.sessions if s is not session):
+ try:
+ identity = CPIMIdentity.parse(format_identity(session.remote_identity, True))
+ chat_stream = (stream for stream in s.streams if stream.type == 'chat').next()
+ chat_stream.send_message(message.body, message.content_type, local_identity=identity, recipients=[self.identity], timestamp=message.timestamp)
+ except ChatStreamError, e:
+ log.error('Error dispatching message to %s: %s' % (s.remote_identity.uri, e))
+ except StopIteration:
+ # This session doesn't have a chat stream, send him a SIP MESSAGE
+ if ConferenceConfig.enable_sip_message:
+ self.send_sip_message(session.remote_identity.uri, s.remote_identity.uri, message.content_type, message.body)
+
+ def dispatch_private_message(self, session, message):
+ # Private messages are delivered to all sessions matching the recipient but also to the sender,
+ # for replication in clients
+ recipient = message.recipients[0]
+ for s in (s for s in self.sessions if s is not session and s.remote_identity.uri in (recipient.uri, session.remote_identity.uri)):
+ try:
+ identity = CPIMIdentity.parse(format_identity(session.remote_identity, True))
+ chat_stream = (stream for stream in s.streams if stream.type == 'chat').next()
+ chat_stream.send_message(message.body, message.content_type, local_identity=identity, recipients=[recipient], timestamp=message.timestamp)
+ except ChatStreamError, e:
+ log.error('Error dispatching private message to %s: %s' % (s.remote_identity.uri, e))
+
+ def dispatch_iscomposing(self, session, data):
+ for s in (s for s in self.sessions if s is not session):
+ try:
+ identity = CPIMIdentity.parse(format_identity(session.remote_identity, True))
+ chat_stream = (stream for stream in s.streams if stream.type == 'chat').next()
+ chat_stream.send_composing_indication(data.state, data.refresh, data.last_active, local_identity=identity, recipients=[self.identity])
+ except ChatStreamError, e:
+ log.error('Error dispatching composing indication to %s: %s' % (s.remote_identity.uri, e))
+ except StopIteration:
+ # This session doesn't have a chat stream, send him a SIP MESSAGE
+ if ConferenceConfig.enable_sip_message:
+ body = IsComposingMessage(state=State(data.state), refresh=Refresh(data.refresh), last_active=LastActive(data.last_active or datetime.now()), content_type=ContentType('text')).toxml()
+ self.send_sip_message(session.remote_identity.uri, s.remote_identity.uri, IsComposingMessage.content_type, body)
+
+ def dispatch_private_iscomposing(self, session, data):
+ recipient_uri = data.recipients[0].uri
+ for s in (s for s in self.sessions if s is not session and s.remote_identity.uri == recipient_uri):
+ try:
+ identity = CPIMIdentity.parse(format_identity(session.remote_identity, True))
+ chat_stream = (stream for stream in s.streams if stream.type == 'chat').next()
+ chat_stream.send_composing_indication(data.state, data.refresh, data.last_active, local_identity=identity)
+ except ChatStreamError, e:
+ log.error('Error dispatching private composing indication to %s: %s' % (s.remote_identity.uri, e))
+
+ def dispatch_server_message(self, body, content_type='text/plain', exclude=None):
+ for session in (session for session in self.sessions if session is not exclude):
+ try:
+ chat_stream = (stream for stream in session.streams if stream.type == 'chat').next()
+ chat_stream.send_message(body, content_type, local_identity=self.identity, recipients=[self.identity])
+ except StopIteration:
+ # This session doesn't have a chat stream, send him a SIP MESSAGE
+ if ConferenceConfig.enable_sip_message:
+ self.send_sip_message(self.identity.uri, session.remote_identity.uri, content_type, body)
+ self_identity = format_identity(self.identity, cpim_format=True)
+ database.async_save_message(self_identity, self.uri, body, content_type, self_identity, self_identity, datetime.now())
+
+ def dispatch_conference_info(self):
+ data = self.build_conference_info_payload()
+ for subscription in (subscription for subscription in self.subscriptions if subscription.state == 'active'):
+ try:
+ subscription.push_content(Conference.content_type, data)
+ except SIPCoreInvalidStateError:
+ pass
+
+ @run_in_green_thread
+ def send_sip_message(self, from_uri, to_uri, content_type, body):
+ lookup = DNSLookup()
+ settings = SIPSimpleSettings()
+ try:
+ routes = lookup.lookup_sip_proxy(to_uri, settings.sip.transport_list).wait()
+ except DNSLookupError:
+ log.warning('DNS lookup error while looking for %s proxy' % to_uri)
+ else:
+ route = routes.pop(0)
+ from_header = FromHeader(self.identity.uri)
+ to_header = ToHeader(SIPURI.new(to_uri))
+ route_header = RouteHeader(route.get_uri())
+ sender = CPIMIdentity(from_uri)
+ for chunk in chunks(body, 1000):
+ msg = CPIMMessage(chunk, content_type, sender=sender, recipients=[self.identity])
+ message_request = Message(from_header, to_header, route_header, 'message/cpim', str(msg))
+ message_request.send()
+
+ def render_text_welcome(self, session):
+ txt = 'Welcome to the conference.'
+ user_count = len(set(str(s.remote_identity.uri) for s in self.sessions) - set([str(session.remote_identity.uri)]))
+ if user_count == 0:
+ txt += ' You are the first participant in the room.'
+ else:
+ if user_count == 1:
+ txt += ' There is one more participant in the room.'
+ else:
+ txt += ' There are %s more participants in the room.' % user_count
+ return txt
+
+ def _play_file_in_player(self, player, file, delay):
+ player.filename = str(file)
+ player.pause_time = delay
+ try:
+ player.play().wait()
+ except WavePlayerError, e:
+ log.warning("Error playing file %s: %s" % (file, e))
+
+ @run_in_green_thread
+ def play_audio_welcome(self, session, play_welcome=True):
+ audio_stream = (stream for stream in session.streams if stream.type == 'audio').next()
+ player = WavePlayer(audio_stream.mixer, '', pause_time=1, initial_play=False, volume=50)
+ audio_stream.bridge.add(player)
+ if play_welcome:
+ file = ResourcePath('sounds/co_welcome_conference.wav').normalized
+ self._play_file_in_player(player, file, 1)
+ user_count = len(set(str(s.remote_identity.uri) for s in self.sessions if any(stream for stream in s.streams if stream.type == 'audio')) - set([str(session.remote_identity.uri)]))
+ if user_count == 0:
+ file = ResourcePath('sounds/co_only_one.wav').normalized
+ self._play_file_in_player(player, file, 0.5)
+ elif user_count == 1:
+ file = ResourcePath('sounds/co_there_is.wav').normalized
+ self._play_file_in_player(player, file, 0.5)
+ elif user_count < 100:
+ file = ResourcePath('sounds/co_there_are.wav').normalized
+ self._play_file_in_player(player, file, 0.2)
+ if user_count <= 24:
+ file = ResourcePath('sounds/bi_%d.wav' % user_count).normalized
+ self._play_file_in_player(player, file, 0.1)
+ else:
+ file = ResourcePath('sounds/bi_%d0.wav' % (user_count / 10)).normalized
+ self._play_file_in_player(player, file, 0.1)
+ file = ResourcePath('sounds/bi_%d.wav' % (user_count % 10)).normalized
+ self._play_file_in_player(player, file, 0.1)
+ file = ResourcePath('sounds/co_more_participants.wav').normalized
+ self._play_file_in_player(player, file, 0)
+ audio_stream.bridge.remove(player)
+ self.audio_conference.add(audio_stream)
+ self.audio_conference.unhold()
+ if len(self.audio_conference.streams) == 1:
+ # Play MoH
+ self.moh_player.play()
+ else:
+ self.moh_player.pause()
+
+ def add_session(self, session):
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, sender=session)
+ self.sessions.append(session)
+ try:
+ chat_stream = (stream for stream in session.streams if stream.type == 'chat').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.add_observer(self, sender=chat_stream)
+ remote_identity = CPIMIdentity.parse(format_identity(session.remote_identity, cpim_format=True))
+ # getting last messages may take time, so new messages can arrive before messages the last message from history
+ for msg in database.get_last_messages(self.uri, ConferenceConfig.replay_history):
+ recipient = CPIMIdentity.parse(msg.cpim_recipient)
+ sender = CPIMIdentity.parse(msg.cpim_sender)
+ if recipient.uri in (self.identity.uri, remote_identity.uri) or sender.uri == remote_identity.uri:
+ chat_stream.send_message(msg.cpim_body, msg.cpim_content_type, local_identity=sender, recipients=[recipient], timestamp=msg.cpim_timestamp)
+ try:
+ audio_stream = (stream for stream in session.streams if stream.type == 'audio').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.add_observer(self, sender=audio_stream)
+ log.msg('Audio stream using %s/%sHz (%s), end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate,
+ 'encrypted' if audio_stream.srtp_active else 'unencrypted',
+ audio_stream.local_rtp_address, audio_stream.local_rtp_port,
+ audio_stream.remote_rtp_address, audio_stream.remote_rtp_port))
+ self.play_audio_welcome(session)
+ self.dispatch_conference_info()
+ if len(self.sessions) == 1:
+ log.msg('%s started conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
+ else:
+ log.msg('%s joined conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams)))
+ if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
+ self.dispatch_server_message('%s has joined the room %s' % (format_identity(session.remote_identity), format_stream_types(session.streams)), exclude=session)
+
+ def remove_session(self, session):
+ notification_center = NotificationCenter()
+ try:
+ chat_stream = (stream for stream in session.streams or [] if stream.type == 'chat').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.remove_observer(self, sender=chat_stream)
+ try:
+ audio_stream = (stream for stream in session.streams or [] if stream.type == 'audio').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.remove_observer(self, sender=audio_stream)
+ try:
+ self.audio_conference.remove(audio_stream)
+ except ValueError:
+ # User may hangup before getting bridged into the conference
+ pass
+ if len(self.audio_conference.streams) == 0:
+ self.moh_player.pause()
+ self.audio_conference.hold()
+ elif len(self.audio_conference.streams) == 1:
+ self.moh_player.play()
+ notification_center.remove_observer(self, sender=session)
+ self.sessions.remove(session)
+ self.dispatch_conference_info()
+ log.msg('%s left conference %s after %s' % (format_identity(session.remote_identity), self.uri, format_session_duration(session)))
+ if not self.sessions:
+ log.msg('Last participant left conference %s' % self.uri)
+ if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session):
+ self.dispatch_server_message('%s has left the room after %s' % (format_identity(session.remote_identity), format_session_duration(session)))
+
+ def handle_incoming_sip_message(self, message_request, data):
+ content_type = data.headers.get('Content-Type', Null)[0]
+ from_header = data.headers.get('From', Null)
+ if content_type is Null or from_header is Null:
+ message_request.answer(400)
+ return
+ try:
+ # Take the first session which doesn't have a chat stream. This is needed because the
+ # seession picked up here will later be ignored. It doesn't matter if we ignore a session
+ # without a chat stream, because that means we will send SIP MESSAGE, and it will fork, so
+ # everyone will get it.
+ session = (session for session in self.sessions if str(session.remote_identity.uri) == str(from_header.uri) and any(stream for stream in session.streams if stream.type != 'chat')).next()
+ except StopIteration:
+ # MESSAGE from a user which is not in this room
+ message_request.answer(503)
+ return
+ if content_type == 'message/cpim':
+ try:
+ message = CPIMMessage.parse(data.body)
+ except CPIMParserError:
+ message_request.answer(500)
+ return
+ else:
+ body = message.body
+ content_type = message.content_type
+ sender = message.sender or format_identity(from_header, cpim_format=True)
+ if message.timestamp is not None and isinstance(message.timestamp, Timestamp):
+ timestamp = datetime.fromtimestamp(mktime(message.timestamp.timetuple()))
+ else:
+ timestamp = datetime.now()
+ else:
+ body = data.body
+ sender = format_identity(from_header, cpim_format=True)
+ timestamp = datetime.now()
+ message_request.answer(200)
+
+ if content_type == IsComposingMessage.content_type:
+ return
+
+ log.msg('New incoming MESSAGE from %s' % session.remote_identity.uri)
+ self_identity = format_identity(self.identity, cpim_format=True)
+ message = SIPMessage(sender=sender, recipient=self_identity, content_type=content_type, body=body)
+ message.timestamp = timestamp
+ self.incoming_message_queue.send((session, 'sip_message', message))
+
+ def build_conference_info_payload(self):
+ if self.conference_info_payload is None:
+ settings = SIPSimpleSettings()
+ conference_description = ConferenceDescription(display_text='Ad-hoc conference', free_text='Hosted by %s' % settings.user_agent)
+ host_info = HostInfo(web_page=WebPage('http://sylkserver.com'))
+ self.conference_info_payload = Conference(self.identity.uri, conference_description=conference_description, host_info=host_info, users=Users())
+ user_count = len(set(str(s.remote_identity.uri) for s in self.sessions))
+ self.conference_info_payload.conference_state = ConferenceState(user_count=user_count, active=True)
+ users = Users()
+ for session in self.sessions:
+ try:
+ user = (user for user in users if user.entity == str(session.remote_identity.uri)).next()
+ except StopIteration:
+ user = User(str(session.remote_identity.uri), display_text=session.remote_identity.display_name)
+ users.append(user)
+ joining_info = JoiningInfo(when=session.start_time)
+ holdable_streams = [stream for stream in session.streams if stream.hold_supported]
+ session_on_hold = holdable_streams and all(stream.on_hold_by_remote for stream in holdable_streams)
+ hold_status = EndpointStatus('on-hold' if session_on_hold else 'connected')
+ endpoint = Endpoint(str(session._invitation.remote_contact_header.uri), display_text=session.remote_identity.display_name, joining_info=joining_info, status=hold_status)
+ for stream in session.streams:
+ endpoint.append(Media(id(stream), media_type=format_conference_stream_type(stream)))
+ user.append(endpoint)
+ self.conference_info_payload.users = users
+ return self.conference_info_payload.toxml()
+
+ def handle_incoming_subscription(self, subscribe_request, data):
+ content = self.build_conference_info_payload()
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, sender=subscribe_request)
+ subscribe_request.accept(Conference.content_type, content)
+ self.subscriptions.append(subscribe_request)
+
+ def accept_proposal(self, session, streams):
+ if session in self.sessions_with_proposals:
+ session.accept_proposal(streams)
+ self.sessions_with_proposals.remove(session)
+
+ @run_in_twisted_thread
+ def handle_notification(self, notification):
+ handler = getattr(self, '_NH_%s' % notification.name, Null)
+ handler(notification)
+
+ def _NH_AudioStreamDidTimeout(self, notification):
+ stream = notification.sender
+ session = stream._session
+ log.msg('Audio stream for session %s timed out' % format_identity(session.remote_identity))
+ if session.streams == [stream]:
+ session.end()
+
+ def _NH_ChatStreamGotMessage(self, notification):
+ data = notification.data.message
+ session = notification.sender.session
+ self.incoming_message_queue.send((session, 'message', data))
+
+ def _NH_ChatStreamGotComposingIndication(self, notification):
+ data = notification.data
+ session = notification.sender.session
+ self.incoming_message_queue.send((session, 'composing_indication', data))
+
+ def _NH_SIPIncomingSubscriptionDidEnd(self, notification):
+ subscription = notification.sender
+ notification_center = NotificationCenter()
+ notification_center.remove_observer(self, sender=subscription)
+ self.subscriptions.remove(subscription)
+
+ def _NH_SIPSessionDidChangeHoldState(self, notification):
+ session = notification.sender
+ if notification.data.originator == 'remote':
+ if notification.data.on_hold:
+ log.msg('%s has put the audio session on hold' % format_identity(session.remote_identity))
+ else:
+ log.msg('%s has taken the audio session out of hold' % format_identity(session.remote_identity))
+ self.dispatch_conference_info()
+
+ def _NH_SIPSessionGotProposal(self, notification):
+ session = notification.sender
+ audio_streams = [stream for stream in notification.data.streams if stream.type=='audio']
+ chat_streams = [stream for stream in notification.data.streams if stream.type=='chat']
+ if not audio_streams and not chat_streams:
+ session.reject_proposal()
+ return
+ streams = [streams[0] for streams in (audio_streams, chat_streams) if streams]
+ self.sessions_with_proposals.append(session)
+ reactor.callLater(4, self.accept_proposal, session, streams)
+
+ def _NH_SIPSessionGotRejectProposal(self, notification):
+ session = notification.sender
+ self.sessions_with_proposals.remove(session)
+
+ def _NH_SIPSessionDidRenegotiateStreams(self, notification):
+ notification_center = NotificationCenter()
+ session = notification.sender
+ streams = notification.data.streams
+ if notification.data.action == 'add':
+ try:
+ chat_stream = (stream for stream in streams if stream.type == 'chat').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.add_observer(self, sender=chat_stream)
+ remote_identity = CPIMIdentity.parse(format_identity(session.remote_identity, cpim_format=True))
+ # getting last messages may take time, so new messages can arrive before messages the last message from history
+ for msg in database.get_last_messages(self.uri, ConferenceConfig.replay_history):
+ recipient = CPIMIdentity.parse(msg.cpim_recipient)
+ sender = CPIMIdentity.parse(msg.cpim_sender)
+ if recipient.uri in (self.identity.uri, remote_identity.uri) or sender.uri == remote_identity.uri:
+ chat_stream.send_message(msg.cpim_body, msg.cpim_content_type, local_identity=sender, recipients=[recipient], timestamp=msg.cpim_timestamp)
+ log.msg('%s has added chat to %s' % (format_identity(session.remote_identity), self.uri))
+ self.dispatch_server_message('%s has added chat' % format_identity(session.remote_identity), exclude=session)
+ try:
+ audio_stream = (stream for stream in streams if stream.type == 'audio').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.add_observer(self, sender=audio_stream)
+ log.msg('Audio stream using %s/%sHz (%s), end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate,
+ 'encrypted' if audio_stream.srtp_active else 'unencrypted',
+ audio_stream.local_rtp_address, audio_stream.local_rtp_port,
+ audio_stream.remote_rtp_address, audio_stream.remote_rtp_port))
+ log.msg('%s has added audio to %s' % (format_identity(session.remote_identity), self.uri))
+ self.dispatch_server_message('%s has added audio' % format_identity(session.remote_identity), exclude=session)
+ self.play_audio_welcome(session, False)
+ elif notification.data.action == 'remove':
+ try:
+ chat_stream = (stream for stream in streams if stream.type == 'chat').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.remove_observer(self, sender=chat_stream)
+ log.msg('%s has removed chat from %s' % (format_identity(session.remote_identity), self.uri))
+ self.dispatch_server_message('%s has removed chat' % format_identity(session.remote_identity), exclude=session)
+ try:
+ audio_stream = (stream for stream in streams if stream.type == 'audio').next()
+ except StopIteration:
+ pass
+ else:
+ notification_center.remove_observer(self, sender=audio_stream)
+ try:
+ self.audio_conference.remove(audio_stream)
+ except ValueError:
+ # User may hangup before getting bridged into the conference
+ pass
+ if len(self.audio_conference.streams) == 0:
+ self.moh_player.pause()
+ self.audio_conference.hold()
+ elif len(self.audio_conference.streams) == 1:
+ self.moh_player.play()
+ log.msg('%s has removed audio from %s' % (format_identity(session.remote_identity), self.uri))
+ self.dispatch_server_message('%s has removed audio' % format_identity(session.remote_identity), exclude=session)
+ if not session.streams:
+ log.msg('%s has removed all streams from %s, session will be terminated' % (format_identity(session.remote_identity), self.uri))
+ session.end()
+ self.dispatch_conference_info()
+
+
+class MoHPlayer(object):
+ implements(IObserver)
+
+ def __init__(self, conference):
+ self.conference = conference
+ self.disabled = False
+ self.files = None
+ self.paused = False
+ self._player = None
+
+ def initialize(self):
+ files = glob('%s/*.wav' % ResourcePath('sounds/moh').normalized)
+ if not files:
+ log.error('No files found, MoH is disabled')
+ self.disabled = True
+ return
+ random.shuffle(files)
+ self.files = cycle(files)
+ self._player = WavePlayer(SIPApplication.voice_audio_mixer, '', pause_time=1, initial_play=False, volume=20)
+ self.conference.bridge.add(self._player)
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, sender=self._player)
+
+ def stop(self):
+ if self.disabled:
+ return
+ notification_center = NotificationCenter()
+ notification_center.remove_observer(self, sender=self._player)
+ self.conference.bridge.remove(self, self._player)
+ self._player.stop()
+ self._player = None
+
+ def play(self):
+ if not self.disabled:
+ self.paused = False
+ self._play_next_file()
+ log.msg('Started playing music on hold')
+
+ def pause(self):
+ if not self.disabled:
+ self.paused = True
+ self._player.stop()
+ log.msg('Stopped playing music on hold')
+
+ def _play_next_file(self):
+ file = self.files.next()
+ self._player.filename = str(file)
+ self._player.play()
+
+ @run_in_twisted_thread
+ def handle_notification(self, notification):
+ handler = getattr(self, '_NH_%s' % notification.name, Null)
+ handler(notification)
+
+ def _NH_WavePlayerDidFail(self, notification):
+ if not self.paused:
+ self._play_next_file()
+
+ def _NH_WavePlayerDidEnd(self, notification):
+ if not self.paused:
+ self._play_next_file()
+
diff --git a/sylk/configuration/__init__.py b/sylk/configuration/__init__.py
new file mode 100644
index 0000000..b9f53ec
--- /dev/null
+++ b/sylk/configuration/__init__.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+from application.configuration import ConfigSection, ConfigSetting
+from application.system import host
+from sipsimple.configuration.datatypes import NonNegativeInteger, SRTPEncryption
+
+from sylk import configuration_filename
+from sylk.configuration.datatypes import AudioCodecs, IPAddress, Port, PortRange
+
+
+class ServerConfig(ConfigSection):
+ __cfgfile__ = configuration_filename
+ __section__ = 'Server'
+
+ ca_file = ConfigSetting(type=str, value='/etc/sylkserver/tls/ca.crt')
+ certificate = ConfigSetting(type=str, value='/etc/sylkserver/tls/sylkserver.crt')
+ verify_server = False
+ default_application = 'conference'
+ trace_dir = ConfigSetting(type=str, value='/var/log/sylkserver')
+ trace_sip = False
+ trace_msrp = False
+ trace_notifications = False
+
+
+class SIPConfig(ConfigSection):
+ __cfgfile__ = configuration_filename
+ __section__ = 'SIP'
+
+ local_ip = ConfigSetting(type=IPAddress, value=host.default_ip)
+ local_udp_port = ConfigSetting(type=Port, value=5060)
+ local_tcp_port = ConfigSetting(type=Port, value=5060)
+ local_tls_port = ConfigSetting(type=Port, value=None)
+
+
+class MSRPConfig(ConfigSection):
+ __cfgfile__ = configuration_filename
+ __section__ = 'MSRP'
+
+ use_tls = False
+
+
+class RTPConfig(ConfigSection):
+ __cfgfile__ = configuration_filename
+ __section__ = 'RTP'
+
+ audio_codecs = ConfigSetting(type=AudioCodecs, value=None)
+ port_range = ConfigSetting(type=PortRange, value=PortRange('50000:50500'))
+ srtp_encryption = ConfigSetting(type=SRTPEncryption, value='optional')
+ timeout = ConfigSetting(type=NonNegativeInteger, value=30)
+
+
diff --git a/sylk/configuration/backend.py b/sylk/configuration/backend.py
new file mode 100644
index 0000000..d04d709
--- /dev/null
+++ b/sylk/configuration/backend.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+__all__ = ['MemoryBackend']
+
+from sipsimple.configuration.backend import IConfigurationBackend
+from zope.interface import implements
+
+
+class MemoryBackend(object):
+ """
+ Implementation of a configuration backend that stores data in
+ memory.
+ """
+
+ implements(IConfigurationBackend)
+
+ def __init__(self):
+ self._data = {}
+
+ def load(self):
+ return self._data
+
+ def save(self, data):
+ self._data = data
+
diff --git a/sylk/configuration/datatypes.py b/sylk/configuration/datatypes.py
new file mode 100644
index 0000000..481354d
--- /dev/null
+++ b/sylk/configuration/datatypes.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+import os
+import re
+import socket
+import sys
+
+from sipsimple.configuration.datatypes import AudioCodecList
+from sipsimple.util import classproperty
+
+
+class AudioCodecs(list):
+ def __new__(cls, value):
+ if isinstance(value, (tuple, list)):
+ return [str(x) for x in value if x in AudioCodecList.available_values] or None
+ elif isinstance(value, basestring):
+ if value.lower() in ('none', ''):
+ return None
+ return [x for x in re.split(r'\s*,\s*', value) if x in AudioCodecList.available_values] or None
+ else:
+ raise TypeError("value must be a string, list or tuple")
+
+class IPAddress(str):
+ """An IP address in quad dotted number notation"""
+ def __new__(cls, value):
+ if value == '0.0.0.0':
+ raise ValueError("%s is not allowed, please specify a specific IP address" % value)
+ else:
+ try:
+ socket.inet_aton(value)
+ except socket.error:
+ raise ValueError("invalid IP address: %r" % value)
+ except TypeError:
+ raise TypeError("value must be a string")
+ return str(value)
+
+class ResourcePath(object):
+ def __init__(self, path):
+ self.path = os.path.normpath(str(path))
+
+ def __getstate__(self):
+ return unicode(self.path)
+
+ def __setstate__(self, state):
+ self.__init__(state)
+
+ @property
+ def normalized(self):
+ path = os.path.expanduser(self.path)
+ if os.path.isabs(path):
+ return os.path.realpath(path)
+ return os.path.realpath(os.path.join(self.resources_directory, path))
+
+ @classproperty
+ def resources_directory(cls):
+ binary_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
+ if os.path.basename(binary_directory) == 'bin':
+ application_directory = os.path.dirname(binary_directory)
+ else:
+ application_directory = binary_directory
+ from sipsimple.configuration.settings import SIPSimpleSettings
+ settings = SIPSimpleSettings()
+ if os.path.basename(binary_directory) == 'bin':
+ resources_component = settings.resources_directory or 'share/sylkserver'
+ else:
+ resources_component = settings.resources_directory or 'resources'
+ return os.path.realpath(os.path.join(application_directory, resources_component))
+
+ def __eq__(self, other):
+ try:
+ return self.path == other.path
+ except AttributeError:
+ return False
+
+ def __hash__(self):
+ return hash(self.path)
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.path)
+
+ def __unicode__(self):
+ return unicode(self.path)
+
+class Port(int):
+ def __new__(cls, value):
+ try:
+ value = int(value)
+ except ValueError:
+ return None
+ if not (0 <= value <= 65535):
+ raise ValueError("illegal port value: %s" % value)
+ return value
+
+class PortRange(object):
+ """A port range in the form start:end with start and end being even numbers in the [1024, 65536] range"""
+ def __init__(self, value):
+ self.start, self.end = [int(p) for p in value.split(':', 1)]
+ allowed = xrange(1024, 65537, 2)
+ if not (self.start in allowed and self.end in allowed and self.start < self.end):
+ raise ValueError("bad range: %r: ports must be even numbers in the range [1024, 65536] with start < end" % value)
+
+
diff --git a/sylk/configuration/settings.py b/sylk/configuration/settings.py
new file mode 100644
index 0000000..f9090a4
--- /dev/null
+++ b/sylk/configuration/settings.py
@@ -0,0 +1,121 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+"""
+SIP SIMPLE SDK settings extensions.
+"""
+
+__all__ = ['AccountExtension', 'BonjourAccountExtension', 'SylkServerSettingsExtension']
+
+from sipsimple.account import MSRPSettings as AccountMSRPSettings, NATTraversalSettings as AccountNATTraversalSettings
+from sipsimple.account import RTPSettings as AccountRTPSettings, SIPSettings as AccountSIPSettings, TLSSettings as AccountTLSSettings
+from sipsimple.configuration import CorrelatedSetting, Setting, SettingsObjectExtension
+from sipsimple.configuration.datatypes import AudioCodecList, MSRPTransport, NonNegativeInteger, Path, Port, PortRange, SampleRate, SIPTransportList, SRTPEncryption
+from sipsimple.configuration.settings import AudioSettings, LogsSettings, RTPSettings, SIPSettings, TLSSettings
+
+from sylk import __version__ as server_version
+from sylk.configuration import ServerConfig, SIPConfig, MSRPConfig, RTPConfig
+
+
+# Account settings extensions
+
+msrp_transport = transport = 'tls' if MSRPConfig.use_tls else 'tcp'
+class AccountMSRPSettingsExtension(AccountMSRPSettings):
+ transport = Setting(type=MSRPTransport, default=msrp_transport)
+
+class AccountNATTraversalSettingsExtension(AccountNATTraversalSettings):
+ use_msrp_relay_for_inbound = Setting(type=bool, default=False)
+ use_msrp_relay_for_outbound = Setting(type=bool, default=False)
+
+
+class AccountRTPSettingsExtension(AccountRTPSettings):
+ audio_codec_list = Setting(type=AudioCodecList, default=RTPConfig.audio_codecs, nillable=True)
+ srtp_encryption = Setting(type=SRTPEncryption, default=RTPConfig.srtp_encryption)
+ use_srtp_without_tls = Setting(type=bool, default=True)
+
+
+class AccountSIPSettingsExtension(AccountSIPSettings):
+ register = Setting(type=bool, default=False)
+
+
+tls_certificate = Path(ServerConfig.certificate) if ServerConfig.certificate else None
+class AccountTLSSettingsExtension(AccountTLSSettings):
+ certificate = Setting(type=Path, default=tls_certificate, nillable=True)
+ verify_server = Setting(type=bool, default=ServerConfig.verify_server)
+
+class AccountExtension(SettingsObjectExtension):
+ enabled = Setting(type=bool, default=True)
+
+ msrp = AccountMSRPSettingsExtension
+ nat_traversal = AccountNATTraversalSettingsExtension
+ rtp = AccountRTPSettingsExtension
+ sip = AccountSIPSettingsExtension
+ tls = AccountTLSSettingsExtension
+
+
+class BonjourAccountExtension(SettingsObjectExtension):
+ enabled = Setting(type=bool, default=False)
+
+
+
+# General settings extensions
+
+class AudioSettingsExtension(AudioSettings):
+ input_device = Setting(type=str, default=None, nillable=True)
+ output_device = Setting(type=str, default=None, nillable=True)
+ sample_rate = Setting(type=SampleRate, default=16000)
+
+
+log_directory = Path(ServerConfig.trace_dir) if ServerConfig.trace_dir else None
+class LogsSettingsExtension(LogsSettings):
+ directory = Setting(type=Path, default=log_directory)
+ trace_sip = Setting(type=bool, default=ServerConfig.trace_sip)
+ trace_msrp = Setting(type=bool, default=ServerConfig.trace_msrp)
+ trace_pjsip = Setting(type=bool, default=True)
+ trace_notifications = Setting(type=bool, default=ServerConfig.trace_notifications)
+
+
+class RTPSettingsExtension(RTPSettings):
+ port_range = Setting(type=PortRange, default=PortRange(RTPConfig.port_range.start, RTPConfig.port_range.end))
+ timeout = Setting(type=NonNegativeInteger, default=RTPConfig.timeout)
+
+
+def sip_port_validator(port, sibling_port):
+ if port == sibling_port != 0:
+ raise ValueError("the TCP and TLS ports must be different")
+
+transport_list = []
+if SIPConfig.local_udp_port:
+ transport_list.append('udp')
+if SIPConfig.local_tcp_port:
+ transport_list.append('tcp')
+if SIPConfig.local_tls_port:
+ transport_list.append('tls')
+
+udp_port = SIPConfig.local_udp_port or 0
+tcp_port = SIPConfig.local_tcp_port or 0
+tls_port = SIPConfig.local_tls_port or 0
+
+class SIPSettingsExtension(SIPSettings):
+ udp_port = Setting(type=Port, default=udp_port)
+ tcp_port = CorrelatedSetting(type=Port, sibling='tls_port', validator=sip_port_validator, default=tcp_port)
+ tls_port = CorrelatedSetting(type=Port, sibling='tcp_port', validator=sip_port_validator, default=tls_port)
+ transport_list = Setting(type=SIPTransportList, default=transport_list)
+
+
+ca_list = Path(ServerConfig.ca_file) if ServerConfig.ca_file else None
+class TLSSettingsExtension(TLSSettings):
+ ca_list = Setting(type=Path, default=ca_list, nillable=True)
+
+
+class SylkServerSettingsExtension(SettingsObjectExtension):
+ resources_directory = Setting(type=Path, default=None, nillable=True)
+ user_agent = Setting(type=str, default='SylkServer-%s' % server_version)
+
+ audio = AudioSettingsExtension
+ logs = LogsSettingsExtension
+ rtp = RTPSettingsExtension
+ sip = SIPSettingsExtension
+ tls = TLSSettingsExtension
+
+
diff --git a/sylk/database.py b/sylk/database.py
new file mode 100644
index 0000000..7769fad
--- /dev/null
+++ b/sylk/database.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+"""
+Database connection factory
+
+Each application that wants to connect to a database should instantiate a Database
+object with the URI it wants to connect to. As Database is a Singleton, same object
+will be used if the same URI is specified.
+
+A usage example can be found in the conference application database module.
+"""
+
+__all__ = ['Database']
+
+from application import log
+from application.python.util import Null, Singleton
+from sqlobject import connectionForURI
+
+
+class Database(object):
+ __metaclass__ = Singleton
+
+ def __init__(self, uri):
+ if uri == 'sqlite:/:memory:':
+ log.warn("SQLite memory backend can't be used because it's not thread-safe")
+ uri = None
+ self.uri = uri
+ if uri is not None:
+ self.connection = connectionForURI(uri)
+ else:
+ self.connection = Null
+
+ def create_table(self, klass):
+ if klass._connection is Null or klass.tableExists():
+ return
+ else:
+ log.warn('Table %s does not exists. Creating it now.' % klass.sqlmeta.table)
+ saved = klass._connection.debug
+ try:
+ klass._connection.debug = True # log SQL used to create the table
+ klass.createTable()
+ finally:
+ klass._connection.debug = saved
+
diff --git a/sylk/extensions.py b/sylk/extensions.py
new file mode 100644
index 0000000..455ee40
--- /dev/null
+++ b/sylk/extensions.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+import random
+
+from datetime import datetime
+
+from msrplib.session import contains_mime_type
+from sipsimple.account import AccountManager
+from sipsimple.core import SDPAttribute
+from sipsimple.payloads.iscomposing import IsComposingMessage, State, LastActive, Refresh, ContentType
+from sipsimple.streams import MediaStreamRegistry
+from sipsimple.streams.applications.chat import CPIMMessage
+from sipsimple.streams.msrp import ChatStream as _ChatStream, ChatStreamError, MSRPStreamBase
+
+
+# We need to match on the only account that will be available
+def _always_find_default_account(self, contact_uri):
+ return self.default_account
+AccountManager.find_account = _always_find_default_account
+
+
+# We need to be able to set the local identity in the message CPIM envelope
+# so that messages appear to be coming from the users themselves, instead of
+# just seeying the server identity
+registry = MediaStreamRegistry()
+for stream_type in registry.stream_types[:]:
+ if stream_type is _ChatStream:
+ registry.stream_types.remove(stream_type)
+ break
+del registry
+
+class ChatStream(_ChatStream):
+ accept_types = ['message/cpim']
+ accept_wrapped_types = ['*']
+
+ def _create_local_media(self, uri_path):
+ local_media = MSRPStreamBase._create_local_media(self, uri_path)
+ if self.session.local_focus:
+ local_media.attributes.append(SDPAttribute('chatroom', 'private-messages'))
+ return local_media
+
+ def send_message(self, content, content_type='text/plain', local_identity=None, recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None):
+ if self.direction=='recvonly':
+ raise ChatStreamError('Cannot send message on recvonly stream')
+ message_id = '%x' % random.getrandbits(64)
+ if not contains_mime_type(self.accept_wrapped_types, content_type):
+ raise ChatStreamError('Invalid content_type for outgoing message: %r' % content_type)
+ if not recipients:
+ recipients = [self.remote_identity]
+ if timestamp is None:
+ timestamp = datetime.now()
+ # Only use CPIM, it's the only type we accept
+ msg = CPIMMessage(content, content_type, sender=local_identity or self.local_identity, recipients=recipients, courtesy_recipients=courtesy_recipients,
+ subject=subject, timestamp=timestamp, required=required, additional_headers=additional_headers)
+ self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='yes', success_report='yes', notify_progress=True)
+ return message_id
+
+ def send_composing_indication(self, state, refresh, last_active=None, recipients=None, local_identity=None):
+ if self.direction == 'recvonly':
+ raise ChatStreamError('Cannot send message on recvonly stream')
+ if state not in ('active', 'idle'):
+ raise ValueError('Invalid value for composing indication state')
+ message_id = '%x' % random.getrandbits(64)
+ content = IsComposingMessage(state=State(state), refresh=Refresh(refresh), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml()
+ if recipients is None:
+ recipients = [self.remote_identity]
+ # Only use CPIM, it's the only type we accept
+ msg = CPIMMessage(content, IsComposingMessage.content_type, sender=local_identity or self.local_identity, recipients=recipients, timestamp=datetime.now())
+ self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='partial', success_report='no')
+ return message_id
+
+
diff --git a/sylk/log.py b/sylk/log.py
new file mode 100644
index 0000000..66401e6
--- /dev/null
+++ b/sylk/log.py
@@ -0,0 +1,307 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+"""
+Logging support adapted from SIP SIMPLE Client logger.
+"""
+
+from __future__ import with_statement
+
+__all__ = ["Logger"]
+
+import datetime
+import os
+import sys
+
+from pprint import pformat
+
+from application import log
+from application.notification import IObserver, NotificationCenter
+from application.python.queue import EventQueue
+from application.python.util import Null
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.util import makedirs
+from zope.interface import implements
+
+
+class Logger(object):
+ implements(IObserver)
+
+ # public methods
+ #
+
+ def __init__(self, msrp_level=log.level.ERROR):
+ self.msrp_level = msrp_level
+
+ self._siptrace_filename = None
+ self._siptrace_file = None
+ self._siptrace_error = False
+ self._siptrace_start_time = None
+ self._siptrace_packet_count = 0
+
+ self._msrptrace_filename = None
+ self._msrptrace_file = None
+ self._msrptrace_error = False
+
+ self._pjsiptrace_filename = None
+ self._pjsiptrace_file = None
+ self._pjsiptrace_error = False
+
+ self._notifications_filename = None
+ self._notifications_file = None
+ self._notifications_error = False
+
+ self._event_queue = EventQueue(handler=self._process_notification, name='Log handling')
+ self._log_directory_error = False
+
+ def start(self):
+ # try to create the log directory
+ try:
+ self._init_log_directory()
+ except Exception:
+ pass
+
+ # register to receive log notifications
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self)
+
+ # start the thread processing the notifications
+ self._event_queue.start()
+
+ def stop(self):
+ # stop the thread processing the notifications
+ self._event_queue.stop()
+ self._event_queue.join()
+
+ # close sip trace file
+ if self._siptrace_file is not None:
+ self._siptrace_file.close()
+ self._siptrace_file = None
+
+ # close msrp trace file
+ if self._msrptrace_file is not None:
+ self._msrptrace_file.close()
+ self._msrptrace_file = None
+
+ # close pjsip trace file
+ if self._pjsiptrace_file is not None:
+ self._pjsiptrace_file.close()
+ self._pjsiptrace_file = None
+
+ # close notifications trace file
+ if self._notifications_file is not None:
+ self._notifications_file.close()
+ self._notifications_file = None
+
+ # unregister from receiving notifications
+ notification_center = NotificationCenter()
+ notification_center.remove_observer(self)
+
+ def handle_notification(self, notification):
+ self._event_queue.put(notification)
+
+ def _process_notification(self, notification):
+ settings = SIPSimpleSettings()
+ handler = getattr(self, '_NH_%s' % notification.name, Null)
+ handler(notification)
+
+ handler = getattr(self, '_LH_%s' % notification.name, Null)
+ handler(notification)
+
+ if notification.name not in ('SIPEngineLog', 'SIPEngineSIPTrace') and settings.logs.trace_notifications:
+ message = 'Notification name=%s sender=%s' % (notification.name, notification.sender)
+ if notification.data is not None:
+ message += '\n%s' % pformat(notification.data.__dict__)
+ if settings.logs.trace_notifications:
+ try:
+ self._init_log_file('notifications')
+ except Exception:
+ pass
+ else:
+ self._notifications_file.write('%s [%s %d]: %s\n' % (datetime.datetime.now(), os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._notifications_file.flush()
+
+ # notification handlers
+ #
+
+ def _NH_CFGSettingsObjectDidChange(self, notification):
+ settings = SIPSimpleSettings()
+ if notification.sender is settings:
+ if 'logs.directory' in notification.data.modified:
+ # sip trace
+ if self._siptrace_file is not None:
+ self._siptrace_file.close()
+ self._siptrace_file = None
+ # pjsip trace
+ if self._pjsiptrace_file is not None:
+ self._pjsiptrace_file.close()
+ self._pjsiptrace_file = None
+ # notifications trace
+ if self._notifications_file is not None:
+ self._notifications_file.close()
+ self._notifications_file = None
+ # try to create the log directory
+ try:
+ self._init_log_directory()
+ except Exception:
+ pass
+
+ # log handlers
+ #
+
+ def _LH_SIPEngineSIPTrace(self, notification):
+ settings = SIPSimpleSettings()
+ if not settings.logs.trace_sip:
+ return
+ if self._siptrace_start_time is None:
+ self._siptrace_start_time = notification.data.timestamp
+ self._siptrace_packet_count += 1
+ if notification.data.received:
+ direction = "RECEIVED"
+ else:
+ direction = "SENDING"
+ buf = ["%s: Packet %d, +%s" % (direction, self._siptrace_packet_count, (notification.data.timestamp - self._siptrace_start_time))]
+ buf.append("%(source_ip)s:%(source_port)d -(SIP over %(transport)s)-> %(destination_ip)s:%(destination_port)d" % notification.data.__dict__)
+ buf.append(notification.data.data)
+ buf.append('--')
+ message = '\n'.join(buf)
+ if settings.logs.trace_sip:
+ try:
+ self._init_log_file('siptrace')
+ except Exception:
+ pass
+ else:
+ self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._siptrace_file.flush()
+
+ def _LH_SIPEngineLog(self, notification):
+ settings = SIPSimpleSettings()
+ if not settings.logs.trace_pjsip:
+ return
+ message = "(%(level)d) %(sender)14s: %(message)s" % notification.data.__dict__
+ if settings.logs.trace_pjsip:
+ try:
+ self._init_log_file('pjsiptrace')
+ except Exception:
+ pass
+ else:
+ self._pjsiptrace_file.write('%s [%s %d] %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._pjsiptrace_file.flush()
+
+ def _LH_DNSLookupTrace(self, notification):
+ settings = SIPSimpleSettings()
+ if not settings.logs.trace_sip:
+ return
+ message = 'DNS lookup %(query_type)s %(query_name)s' % notification.data.__dict__
+ if notification.data.error is None:
+ message += ' succeeded, ttl=%d: ' % notification.data.answer.ttl
+ if notification.data.query_type == 'A':
+ message += ", ".join(record.address for record in notification.data.answer)
+ elif notification.data.query_type == 'SRV':
+ message += ", ".join('%d %d %d %s' % (record.priority, record.weight, record.port, record.target) for record in notification.data.answer)
+ elif notification.data.query_type == 'NAPTR':
+ message += ", ".join('%d %d "%s" "%s" "%s" %s' % (record.order, record.preference, record.flags, record.service, record.regexp, record.replacement) for record in notification.data.answer)
+ else:
+ import dns.resolver
+ message_map = {dns.resolver.NXDOMAIN: 'DNS record does not exist',
+ dns.resolver.NoAnswer: 'DNS response contains no answer',
+ dns.resolver.NoNameservers: 'no DNS name servers could be reached',
+ dns.resolver.Timeout: 'no DNS response received, the query has timed out'}
+ message += ' failed: %s' % message_map.get(notification.data.error.__class__, '')
+ if settings.logs.trace_sip:
+ try:
+ self._init_log_file('siptrace')
+ except Exception:
+ pass
+ else:
+ self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._siptrace_file.flush()
+
+ def _LH_MSRPTransportTrace(self, notification):
+ settings = SIPSimpleSettings()
+ if not settings.logs.trace_msrp:
+ return
+ arrow = {'incoming': '<--', 'outgoing': '-->'}[notification.data.direction]
+ local_address = notification.sender.getHost()
+ local_address = '%s:%d' % (local_address.host, local_address.port)
+ remote_address = notification.sender.getPeer()
+ remote_address = '%s:%d' % (remote_address.host, remote_address.port)
+ message = '%s %s %s\n' % (local_address, arrow, remote_address) + notification.data.data
+ if settings.logs.trace_msrp:
+ try:
+ self._init_log_file('msrptrace')
+ except Exception:
+ pass
+ else:
+ self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._msrptrace_file.flush()
+
+ def _LH_MSRPLibraryLog(self, notification):
+ settings = SIPSimpleSettings()
+ if not settings.logs.trace_msrp:
+ return
+ if notification.data.level < self.msrp_level:
+ return
+ message = '%s%s' % (notification.data.level.prefix, notification.data.message)
+ if settings.logs.trace_msrp:
+ try:
+ self._init_log_file('msrptrace')
+ except Exception:
+ pass
+ else:
+ self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+ self._msrptrace_file.flush()
+
+ # private methods
+ #
+
+ def _init_log_directory(self):
+ settings = SIPSimpleSettings()
+ log_directory = settings.logs.directory.normalized
+ try:
+ makedirs(log_directory)
+ except Exception, e:
+ if not self._log_directory_error:
+ print "failed to create logs directory '%s': %s" % (log_directory, e)
+ self._log_directory_error = True
+ self._siptrace_error = True
+ self._pjsiptrace_error = True
+ self._notifications_error = True
+ raise
+ else:
+ self._log_directory_error = False
+ # sip trace
+ if self._siptrace_filename is None:
+ self._siptrace_filename = os.path.join(log_directory, 'sip_trace.txt')
+ self._siptrace_error = False
+
+ # msrp trace
+ if self._msrptrace_filename is None:
+ self._msrptrace_filename = os.path.join(log_directory, 'msrp_trace.txt')
+ self._msrptrace_error = False
+
+ # pjsip trace
+ if self._pjsiptrace_filename is None:
+ self._pjsiptrace_filename = os.path.join(log_directory, 'pjsip_trace.txt')
+ self._pjsiptrace_error = False
+
+ # notifications trace
+ if self._notifications_filename is None:
+ self._notifications_filename = os.path.join(log_directory, 'notifications_trace.txt')
+ self._notifications_error = False
+
+ def _init_log_file(self, type):
+ if getattr(self, '_%s_file' % type) is None:
+ self._init_log_directory()
+ filename = getattr(self, '_%s_filename' % type)
+ try:
+ setattr(self, '_%s_file' % type, open(filename, 'a'))
+ except Exception, e:
+ if not getattr(self, '_%s_error' % type):
+ print "failed to create log file '%s': %s" % (filename, e)
+ setattr(self, '_%s_error' % type, True)
+ raise
+ else:
+ setattr(self, '_%s_error' % type, False)
+
+
diff --git a/sylk/server.py b/sylk/server.py
new file mode 100644
index 0000000..fb62d93
--- /dev/null
+++ b/sylk/server.py
@@ -0,0 +1,172 @@
+# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
+#
+
+import sys
+
+from threading import Event
+
+from application import log
+from application.notification import NotificationCenter
+from sipsimple.account import Account, BonjourAccount, AccountManager
+from sipsimple.application import SIPApplication
+from sipsimple.audio import AudioDevice, RootAudioBridge
+from sipsimple.configuration import ConfigurationError
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.core import AudioMixer, Engine, SIPCoreError
+from sipsimple.session import SessionManager
+from sipsimple.util import TimestampedNotificationData
+from twisted.internet import reactor
+
+from sylk.applications import IncomingRequestHandler
+from sylk.configuration import SIPConfig
+from sylk.configuration.backend import MemoryBackend
+from sylk.configuration.settings import AccountExtension, BonjourAccountExtension, SylkServerSettingsExtension
+from sylk.log import Logger
+
+# Load extensions needed for integration with SIP SIMPLE SDK
+import sylk.extensions
+
+
+class SylkServer(SIPApplication):
+
+ def __init__(self):
+ self.logger = None
+ self.request_handler = IncomingRequestHandler()
+ self.stop_event = Event()
+
+ def start(self):
+ notification_center = NotificationCenter()
+ notification_center.add_observer(self, sender=self)
+
+ self.logger = Logger()
+
+ Account.register_extension(AccountExtension)
+ BonjourAccount.register_extension(BonjourAccountExtension)
+ SIPSimpleSettings.register_extension(SylkServerSettingsExtension)
+
+ try:
+ SIPApplication.start(self, MemoryBackend())
+ except ConfigurationError, e:
+ log.fatal("Error loading configuration: ",e)
+ sys.exit(1)
+
+ def _load_configuration(self):
+ account_manager = AccountManager()
+ account = Account("account@example.com") # an account is required by AccountManager
+ account_manager.default_account = account
+
+ def _initialize_subsystems(self):
+ account_manager = AccountManager()
+ engine = Engine()
+ notification_center = NotificationCenter()
+ session_manager = SessionManager()
+ settings = SIPSimpleSettings()
+ self._load_configuration()
+
+ notification_center.post_notification('SIPApplicationWillStart', sender=self, data=TimestampedNotificationData())
+ if self.state == 'stopping':
+ reactor.stop()
+ return
+
+ account = account_manager.default_account
+
+ # initialize core
+ notification_center.add_observer(self, sender=engine)
+ options = dict(# general
+ ip_address=SIPConfig.local_ip,
+ user_agent=settings.user_agent,
+ # SIP
+ udp_port=settings.sip.udp_port if 'udp' in settings.sip.transport_list else None,
+ tcp_port=settings.sip.tcp_port if 'tcp' in settings.sip.transport_list else None,
+ tls_port=None,
+ # TLS
+ tls_protocol='TLSv1',
+ tls_verify_server=False,
+ tls_ca_file=None,
+ tls_cert_file=None,
+ tls_privkey_file=None,
+ tls_timeout=3000,
+ # rtp
+ rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
+ # audio
+ codecs=list(settings.rtp.audio_codec_list),
+ # logging
+ log_level=settings.logs.pjsip_level,
+ trace_sip=True,
+ # events and requests to handle
+ events={"conference": ["application/conference-info+xml"]},
+ incoming_events=set(['conference']),
+ incoming_requests=set(['MESSAGE'])
+ )
+ try:
+ engine.start(**options)
+ except SIPCoreError:
+ self.end_reason = 'engine failed'
+ reactor.stop()
+ return
+
+ # initialize TLS
+ try:
+ engine.set_tls_options(port=settings.sip.tls_port if 'tls' in settings.sip.transport_list else None,
+ protocol=settings.tls.protocol,
+ verify_server=account.tls.verify_server if account else False,
+ ca_file=settings.tls.ca_list.normalized if settings.tls.ca_list else None,
+ cert_file=account.tls.certificate.normalized if account and account.tls.certificate else None,
+ privkey_file=account.tls.certificate.normalized if account and account.tls.certificate else None,
+ timeout=settings.tls.timeout)
+ except Exception, e:
+ notification_center.post_notification('SIPApplicationFailedToStartTLS', sender=self, data=TimestampedNotificationData(error=e))
+
+ # initialize audio objects
+ voice_mixer = AudioMixer(None, None, settings.audio.sample_rate, settings.audio.tail_length)
+ self.voice_audio_device = AudioDevice(voice_mixer)
+ self.voice_audio_bridge = RootAudioBridge(voice_mixer)
+ self.voice_audio_bridge.add(self.voice_audio_device)
+
+ # initialize middleware components
+ account_manager.start()
+ session_manager.start()
+
+ notification_center.add_observer(self, name='CFGSettingsObjectDidChange')
+
+ self.state = 'started'
+ notification_center.post_notification('SIPApplicationDidStart', sender=self, data=TimestampedNotificationData())
+
+ def _NH_SIPApplicationFailedToStartTLS(self, notification):
+ log.fatal("Couldn't set TLS options: %s" % notification.data.error)
+
+ def _NH_SIPApplicationWillStart(self, notification):
+ self.logger.start()
+ self.request_handler.start()
+ settings = SIPSimpleSettings()
+ if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
+ log.msg('Logging SIP trace to file "%s"' % self.logger._siptrace_filename)
+ if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None:
+ log.msg('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename)
+ if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
+ log.msg('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename)
+ if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
+ log.msg('Logging notifications trace to file "%s"' % self.logger._notifications_filename)
+
+ def _NH_SIPApplicationDidStart(self, notification):
+ engine = Engine()
+ settings = SIPSimpleSettings()
+ local_ip = SIPConfig.local_ip
+ log.msg("SylkServer started, listening on:")
+ for transport in settings.sip.transport_list:
+ try:
+ log.msg("%s:%d (%s)" % (local_ip, getattr(engine, '%s_port' % transport), transport.upper()))
+ except TypeError:
+ pass
+
+ def _NH_SIPApplicationWillEnd(self, notification):
+ self.request_handler.stop()
+
+ def _NH_SIPApplicationDidEnd(self, notification):
+ self.logger.stop()
+ self.stop_event.set()
+
+ def _NH_SIPEngineGotException(self, notification):
+ log.error('An exception occured within the SIP core:\n%s\n' % notification.data.traceback)
+
+