From 0f02e9cd765ce7ede5f7fdd0f44d9d4516cc3a87 Mon Sep 17 00:00:00 2001 From: "psyc://psyced.org/~lynX" <@> Date: Wed, 24 Feb 2010 09:50:45 +0100 Subject: [PATCH] last state we had in cvs --- AGENDA.txt | 70 +++ CONTRIBUTE.txt | 21 + COPYLEFT.txt | 280 +++++++++ FLOW.txt | 49 ++ GUI/Abstract/Gui.py | 29 + GUI/Abstract/Gui.pyc | Bin 0 -> 1791 bytes GUI/Abstract/__init__.py | 1 + GUI/Abstract/__init__.pyc | Bin 0 -> 118 bytes GUI/Tkinter/Gui.py | 448 ++++++++++++++ GUI/Tkinter/__init__.py | 1 + GUI/__init__.py | 1 + GUI/__init__.pyc | Bin 0 -> 122 bytes GUI/wx/Gui.py | 332 +++++++++++ GUI/wx/__init__.py | 1 + GUI/wx/__init__.pyc | Bin 0 -> 125 bytes GUI/wx/devGui.py | 696 ++++++++++++++++++++++ GUI/wx/devGui.pyc | Bin 0 -> 22048 bytes GUI/wx/psych2o.jpg | Bin 0 -> 80224 bytes GUI/wx/test.py | 7 + INSTALL.txt | 24 + LICENSE.txt | 26 + Makefile | 4 + README.txt | 6 + README.wx | 55 ++ config-example | 15 + console-client.py | 99 +++ fippos-twisted/README | 24 + fippos-twisted/contrib/ircd.py | 230 +++++++ fippos-twisted/contrib/rss/README | 9 + fippos-twisted/contrib/rss/rss_server.py | 131 ++++ fippos-twisted/contrib/whitepaper.py | 28 + fippos-twisted/doc/DESIGN | 10 + fippos-twisted/doc/FEATURES | 1 + fippos-twisted/pypsyc/PSYC.py | 70 +++ fippos-twisted/pypsyc/State.py | 156 +++++ fippos-twisted/pypsyc/__init__.py | 101 ++++ fippos-twisted/pypsyc/center.py | 269 +++++++++ fippos-twisted/pypsyc/net.py | 186 ++++++ fippos-twisted/pypsyc/objects/__init__.py | 49 ++ fippos-twisted/pypsyc/objects/client.py | 14 + fippos-twisted/pypsyc/objects/server.py | 319 ++++++++++ fippos-twisted/test.py | 54 ++ gtkpypsyc/client.py | 66 ++ gtkpypsyc/demo.py | 67 +++ gtkpypsyc/list.py | 71 +++ gtkpypsyc/rcfile | 6 + gtkpypsyc/tabs/__init__.py | 0 gtkpypsyc/tabs/place.py | 92 +++ gtkpypsyc/tabs/rss.py | 65 ++ gtkpypsyc/tabs/user.py | 30 + gui-client.py | 70 +++ in-silico/gui/__init__.py | 1 + in-silico/gui/displays.py | 248 ++++++++ in-silico/gui/extras.py | 66 ++ in-silico/gui/psycObjects.py | 297 +++++++++ in-silico/gui/wx.py | 450 ++++++++++++++ in-silico/test.py | 8 + mjacob/TODO.txt | 8 + mjacob/UI/__init__.py | 0 mjacob/UI/__init__.pyc | Bin 0 -> 115 bytes mjacob/UI/wx_.py | 130 ++++ mjacob/UI/wx_.pyc | Bin 0 -> 5468 bytes mjacob/base.py | 143 +++++ mjacob/base.pyc | Bin 0 -> 5329 bytes mjacob/client.py | 87 +++ mjacob/client.pyc | Bin 0 -> 3701 bytes mjacob/lib/__init__.py | 67 +++ mjacob/lib/packets.py | 26 + mjacob/lib/parse.py | 159 +++++ mjacob/lib/render.py | 70 +++ mjacob/old/MMP/MMPProtocol.py | 95 +++ mjacob/old/MMP/__init__.py | 0 mjacob/old/PSYC/PSYCPacket.py | 14 + mjacob/old/PSYC/PSYCProtocol.py | 98 +++ mjacob/old/PSYC/__init__.py | 0 mjacob/old/UI/__init__.py | 0 mjacob/old/UI/base.py | 107 ++++ mjacob/old/UI/text/Interface.py | 18 + mjacob/old/UI/text/__init__.py | 0 mjacob/old/UI/wx/Interface.py | 366 ++++++++++++ mjacob/old/UI/wx/__init__.py | 0 mjacob/old/__init__.py | 36 ++ mjacob/old/pypsyc-twisted.py | 94 +++ mjacob/old/pypsyc.conf | 4 + mjacob/server.py | 62 ++ mjacob/server.pyc | Bin 0 -> 2309 bytes mjacob/socket_/__init__.py | 0 mjacob/socket_/__init__.pyc | Bin 0 -> 120 bytes mjacob/socket_/twisted_.py | 46 ++ mjacob/socket_/twisted_.pyc | Bin 0 -> 1919 bytes mjacob/twisted_client.py | 74 +++ mjacob/twisted_client.pyc | Bin 0 -> 2876 bytes pypsyc/GUI/Gtk/__init__.py | 1 + pypsyc/GUI/Gtk2/__init__.py | 1 + pypsyc/GUI/Qt/Gui.py | 167 ++++++ pypsyc/GUI/Qt/__init__.py | 1 + pypsyc/GUI/__init__.py | 1 + pypsyc/MMP/MMPPacket.py | 28 + pypsyc/MMP/MMPProtocol.py | 84 +++ pypsyc/MMP/MMPProtocol.pyc | Bin 0 -> 3269 bytes pypsyc/MMP/MMPState.py | 124 ++++ pypsyc/MMP/MMPState.pyc | Bin 0 -> 4090 bytes pypsyc/MMP/__init__.py | 8 + pypsyc/MMP/__init__.pyc | Bin 0 -> 382 bytes pypsyc/PSYC/PSYCMessagecenter.py | 169 ++++++ pypsyc/PSYC/PSYCMessagecenter.pyc | Bin 0 -> 6520 bytes pypsyc/PSYC/PSYCProtocol.py | 47 ++ pypsyc/PSYC/PSYCProtocol.pyc | Bin 0 -> 1727 bytes pypsyc/PSYC/PSYCRoom.py | 199 +++++++ pypsyc/PSYC/PSYCRoom.pyc | Bin 0 -> 8676 bytes pypsyc/PSYC/PSYCState.py | 29 + pypsyc/PSYC/PSYCState.pyc | Bin 0 -> 2584 bytes pypsyc/PSYC/__init__.py | 38 ++ pypsyc/PSYC/__init__.pyc | Bin 0 -> 1778 bytes pypsyc/__init__.py | 0 pypsyc/__init__.pyc | Bin 0 -> 125 bytes therapy/COPYLEFT | 280 +++++++++ therapy/INSTALL | 9 + therapy/LICENSE | 27 + therapy/Makefile | 16 + therapy/README | 10 + therapy/TODO | 31 + therapy/therapy | 629 +++++++++++++++++++ therapy/therapyrc-dist | 21 + therapy/ui/__init__.py | 1 + therapy/ui/gtk/__init__.py | 1 + therapy/ui/gtk/ui.py | 237 ++++++++ wx-config-example | 8 + 128 files changed, 9224 insertions(+) create mode 100644 AGENDA.txt create mode 100644 CONTRIBUTE.txt create mode 100644 COPYLEFT.txt create mode 100644 FLOW.txt create mode 100644 GUI/Abstract/Gui.py create mode 100644 GUI/Abstract/Gui.pyc create mode 100644 GUI/Abstract/__init__.py create mode 100644 GUI/Abstract/__init__.pyc create mode 100644 GUI/Tkinter/Gui.py create mode 100644 GUI/Tkinter/__init__.py create mode 100644 GUI/__init__.py create mode 100644 GUI/__init__.pyc create mode 100644 GUI/wx/Gui.py create mode 100644 GUI/wx/__init__.py create mode 100644 GUI/wx/__init__.pyc create mode 100644 GUI/wx/devGui.py create mode 100644 GUI/wx/devGui.pyc create mode 100644 GUI/wx/psych2o.jpg create mode 100644 GUI/wx/test.py create mode 100644 INSTALL.txt create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 README.txt create mode 100644 README.wx create mode 100644 config-example create mode 100755 console-client.py create mode 100644 fippos-twisted/README create mode 100644 fippos-twisted/contrib/ircd.py create mode 100644 fippos-twisted/contrib/rss/README create mode 100644 fippos-twisted/contrib/rss/rss_server.py create mode 100644 fippos-twisted/contrib/whitepaper.py create mode 100644 fippos-twisted/doc/DESIGN create mode 100644 fippos-twisted/doc/FEATURES create mode 100644 fippos-twisted/pypsyc/PSYC.py create mode 100644 fippos-twisted/pypsyc/State.py create mode 100644 fippos-twisted/pypsyc/__init__.py create mode 100644 fippos-twisted/pypsyc/center.py create mode 100644 fippos-twisted/pypsyc/net.py create mode 100644 fippos-twisted/pypsyc/objects/__init__.py create mode 100644 fippos-twisted/pypsyc/objects/client.py create mode 100644 fippos-twisted/pypsyc/objects/server.py create mode 100644 fippos-twisted/test.py create mode 100644 gtkpypsyc/client.py create mode 100644 gtkpypsyc/demo.py create mode 100644 gtkpypsyc/list.py create mode 100644 gtkpypsyc/rcfile create mode 100644 gtkpypsyc/tabs/__init__.py create mode 100644 gtkpypsyc/tabs/place.py create mode 100644 gtkpypsyc/tabs/rss.py create mode 100644 gtkpypsyc/tabs/user.py create mode 100755 gui-client.py create mode 100644 in-silico/gui/__init__.py create mode 100644 in-silico/gui/displays.py create mode 100644 in-silico/gui/extras.py create mode 100644 in-silico/gui/psycObjects.py create mode 100644 in-silico/gui/wx.py create mode 100644 in-silico/test.py create mode 100644 mjacob/TODO.txt create mode 100644 mjacob/UI/__init__.py create mode 100644 mjacob/UI/__init__.pyc create mode 100755 mjacob/UI/wx_.py create mode 100644 mjacob/UI/wx_.pyc create mode 100755 mjacob/base.py create mode 100644 mjacob/base.pyc create mode 100755 mjacob/client.py create mode 100644 mjacob/client.pyc create mode 100755 mjacob/lib/__init__.py create mode 100755 mjacob/lib/packets.py create mode 100755 mjacob/lib/parse.py create mode 100755 mjacob/lib/render.py create mode 100755 mjacob/old/MMP/MMPProtocol.py create mode 100644 mjacob/old/MMP/__init__.py create mode 100755 mjacob/old/PSYC/PSYCPacket.py create mode 100755 mjacob/old/PSYC/PSYCProtocol.py create mode 100644 mjacob/old/PSYC/__init__.py create mode 100644 mjacob/old/UI/__init__.py create mode 100755 mjacob/old/UI/base.py create mode 100755 mjacob/old/UI/text/Interface.py create mode 100644 mjacob/old/UI/text/__init__.py create mode 100755 mjacob/old/UI/wx/Interface.py create mode 100644 mjacob/old/UI/wx/__init__.py create mode 100755 mjacob/old/__init__.py create mode 100755 mjacob/old/pypsyc-twisted.py create mode 100644 mjacob/old/pypsyc.conf create mode 100755 mjacob/server.py create mode 100644 mjacob/server.pyc create mode 100644 mjacob/socket_/__init__.py create mode 100644 mjacob/socket_/__init__.pyc create mode 100755 mjacob/socket_/twisted_.py create mode 100644 mjacob/socket_/twisted_.pyc create mode 100755 mjacob/twisted_client.py create mode 100644 mjacob/twisted_client.pyc create mode 100644 pypsyc/GUI/Gtk/__init__.py create mode 100644 pypsyc/GUI/Gtk2/__init__.py create mode 100644 pypsyc/GUI/Qt/Gui.py create mode 100644 pypsyc/GUI/Qt/__init__.py create mode 100644 pypsyc/GUI/__init__.py create mode 100644 pypsyc/MMP/MMPPacket.py create mode 100644 pypsyc/MMP/MMPProtocol.py create mode 100644 pypsyc/MMP/MMPProtocol.pyc create mode 100644 pypsyc/MMP/MMPState.py create mode 100644 pypsyc/MMP/MMPState.pyc create mode 100644 pypsyc/MMP/__init__.py create mode 100644 pypsyc/MMP/__init__.pyc create mode 100644 pypsyc/PSYC/PSYCMessagecenter.py create mode 100644 pypsyc/PSYC/PSYCMessagecenter.pyc create mode 100644 pypsyc/PSYC/PSYCProtocol.py create mode 100644 pypsyc/PSYC/PSYCProtocol.pyc create mode 100644 pypsyc/PSYC/PSYCRoom.py create mode 100644 pypsyc/PSYC/PSYCRoom.pyc create mode 100644 pypsyc/PSYC/PSYCState.py create mode 100644 pypsyc/PSYC/PSYCState.pyc create mode 100644 pypsyc/PSYC/__init__.py create mode 100644 pypsyc/PSYC/__init__.pyc create mode 100644 pypsyc/__init__.py create mode 100644 pypsyc/__init__.pyc create mode 100644 therapy/COPYLEFT create mode 100644 therapy/INSTALL create mode 100644 therapy/LICENSE create mode 100644 therapy/Makefile create mode 100644 therapy/README create mode 100644 therapy/TODO create mode 100755 therapy/therapy create mode 100644 therapy/therapyrc-dist create mode 100644 therapy/ui/__init__.py create mode 100644 therapy/ui/gtk/__init__.py create mode 100644 therapy/ui/gtk/ui.py create mode 100644 wx-config-example diff --git a/AGENDA.txt b/AGENDA.txt new file mode 100644 index 0000000..337a16d --- /dev/null +++ b/AGENDA.txt @@ -0,0 +1,70 @@ +Meaning of states: +X: done +-: started or halfway done +?: I have no clue what this is about but it sounds important + : nothing done yet but may have a clue how to do it + (note: then make a sketch) + +VERSION 0.1 +=========== + [X] connect to a main psyc UNI via TCP + [X] make it working again automatically, write a package + [X] configfile parsing + [X] implement psyctext parser + [X] actually it is a mmptext-parser as well + [X] parse MMP and PSYCpackets + [ ] implement augment und diminish, but atm muve does not support it + [X] create state objects / machines + [X] handler architecture for psyc functions (requires state objects) + [X] interface definition (taken from bitkoenig's jaPSYC) + [X] basic layout for the classes and objects + [X] support for multiple GUI toolkits by design + [X] make gui main objects differ from application objects + [X] draw a communicative sketch of how it works + [X] debug GUI + +VERSION 0.2 +=========== + [ ] no fullstate sending + [ ] UDP support for some things + [?] what things? listen and wait what comes... + [ ] TCP server listening on port 4404 + [?] authorization checking -> psycauth + [ ] UDP server listening on port 4404 + [ ] same as for tcp + [X] implement graphical room interfaces + [X] use user input + [X] implement graphical user interfaces + [-] implement graphical friendlist + [X] think about command syntax (like: "/" for direct commands, "#" for uni commands, "!" for sending rooms a request_execute + +VERSION 0.3 +=========== + [ ] connect to friend unl's if you want to talk to them and they + are using a psyc interface (ask lynx for remote buddies) + [-] connect to distant rooms (how does it work? ask lynx) + is it even done via a server or is it the clients job? + +VERSION 0.4 +=========== + [ ] parse all messages from lynx's psyced meaningfully + +VERSION 0.5 +=========== + [-] find psyc protocol gagdets and errors and fix them + +VERSION 0.6 +=========== + [ ] make a reference implementation testsuite (tests are good) + that identifies severe and not-so-important lacks in + clients and/or servers. all real psyc clients should support a + minimum set of commands + + +VERSION 1.0 +=========== + [-] "plug-in" gui support (Tkinter, pyGTK, pyQT, wxPython, ...) + Note: basically you only need to implement some classes... + Tkinter GUI may serve as an example for more powerful clients + [-] user-defined handler architecture for "scripting" + ("users" have to write packages anyway) diff --git a/CONTRIBUTE.txt b/CONTRIBUTE.txt new file mode 100644 index 0000000..c204fc8 --- /dev/null +++ b/CONTRIBUTE.txt @@ -0,0 +1,21 @@ + Should you wish to contribute to this program, you can receive a + write access to the software repository from symlynX. By checking + code in, you grant us permission to license your contribution under + both the GNU General Public License and under other licenses. + + With the act of checking into the software repository, you guarantee + that you are either the author and copyright owner of the contribution + you are making, or that you have a license to use said contribution in + a way compatible with our terms of use. If you fail to do so, you + remain the sole responsible for your acting; no part of the + responsibility will be transferred to symlynX. + + This means, for example, that you cannot contribute code elements + which stem from an other GPL project, as this would not comply with + the dual licensing requirement described above. + + + +... if you don't like these terms, feel free to start your own repository ... + or maybe just talk with us about it? + diff --git a/COPYLEFT.txt b/COPYLEFT.txt new file mode 100644 index 0000000..960fe74 --- /dev/null +++ b/COPYLEFT.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + END OF TERMS AND CONDITIONS diff --git a/FLOW.txt b/FLOW.txt new file mode 100644 index 0000000..841cf98 --- /dev/null +++ b/FLOW.txt @@ -0,0 +1,49 @@ + psyc://user@host psyc://host2/@place psyc://host/ + user unl some nice place my uni-host + ^ ^ ^ + | | | + | (connections) | + | | | +----------------------------------------------------------------- +| | +| MMP | +| | +----------------------------------------------------------------- + | ^ + packetReceived() | + (MMP, data) (packet generation) + | (state keeping) + v | +----------------------------------------------------------------- +| | +| PSYC | +| | +----------------------------------------------------------------- + | ^ + packetReceived() | + (MMP, PSYC) | + | | + v | +----------------------------------------------------------------- +| | +| PSYCMessagecenter | +| | +----------------------------------------------------------------- + ^ | ^ | + | received() | received() + register() | register() | + | v | v +--------------------------------- ------------------------------ +| | | | +| PSYCPackage Interface | | GUI | +| | | implementing PSYCPackage | +--------------------------------- ------------------------------ + | + update() + | + v +--------------------------------- +| | +| GUI | +| | +--------------------------------- diff --git a/GUI/Abstract/Gui.py b/GUI/Abstract/Gui.py new file mode 100644 index 0000000..77a49ad --- /dev/null +++ b/GUI/Abstract/Gui.py @@ -0,0 +1,29 @@ +"""Abstract Gui Module - serves as interface reference""" +import sys + +class UserGui: + def __init__(self): + if self.__class__ == UserGui: + raise "class UserGui is abstract!" + def set_model(self, model): self.model = model + + +class RoomGui: + def __init___(self): + if self.__class__ == RoomGui: + raise "class RoomGui is abstract!" + def set_model(self, model): self.model = model + + +class MainGui: + def __init__(self, argv): + if self.__class__ == MainGui: + raise "class MainGui is abstract!" + self.center = None + def run(self): + pass + def quit(self): + self.center.quit() + sys.exit(0) + def connect(self): + pass diff --git a/GUI/Abstract/Gui.pyc b/GUI/Abstract/Gui.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4c933fdffa8b549b2e2c200ea79a4a6429c0843 GIT binary patch literal 1791 zcmc&!&2G~`5T3QurU_IOdO#qA+!HwfIdJFqP$5wtwki_2tc<-uhQtouO+}=hC{MvN za^eY?Z#IcrwMD2GBF$tyJ3D{h%uIi8_ka3d&vRP-V*Y>0V0OOW91MP+#;|@4l(J&0dh)n zXoDiAH%#s!vlF6Hk&ozeM0KAOzyGeNQhXwg8M>*M&nKC4$ufeZkB76x%q{Za{?Xgvsx(7R^XY8iQ8Q!8s`SQe z@Y%r)sU4LgvK2`-+blRyNXQ6qhvGJs6g=Ke>%vY#7%d&Uzyc1T~h@ z0Irt-RoT=U;%e4Uq`->{2?6M_9!`YU@>@37`QXd@F+ z+ighK%MtyS*HvY6{{Ut~o&(hIOfSNjMyHB@jWW08ms7w1@ZDi@Tg", self.on_leftclick) + self.bind("", self.on_rightclick) + + bframe = Tkinter.Frame(root) + bframe.grid(row=1, column = 0, sticky = Tkinter.W + Tkinter.E) + self.linklabel = Tkinter.Label(bframe, text="unlinked") + self.linklabel.grid(row=0, column = 0, sticky = Tkinter.W) + #u = Tkinter.Button(bframe, text="update", command=self.update_friends) + #u.grid(row=0, column = 1, sticky=Tkinter.E) + + def set_model(self, model): self.model = model + + def set_status(self, status): + if status == "_notice_link": + self.linklabel["text"] = "link" + elif status == "_notice_unlink": + self.linklabel["text"] = "unlink" + + def on_leftclick(self, event): + if self.selection_get(): + self.model.set_target(self.selection_get()) + self.model.set_mc("_internal_message_private_window_popup") + self.model.castmsg() + + def on_rightclick(self, event): + if self.selection_get(): + #print "we should pop up a context menu for", self.selection_get() + self.model.set_target(self.selection_get()) + self.model.set_mc("_request_examine") + self.model.send() + def received(self, source, mc, mmp, psyc): + # das print-zeugs is doch nicht ganz so nutzlos! + # muss es aber werden, oder? + if mc == "_notice_friend_present": + self.present(mmp.get_source()) + elif mc == "_notice_friend_absent": + self.absent(mmp.get_source()) + elif mc == "_print_notice_friend_present": + #print "friend present", psyc._query("_friends") + pass + elif mc == "_print_notice_friend_absent": + #print "friend absent", psyc._query("_friends") + pass + elif mc == "_print_list_friends_present": + #print "friends present", psyc._query("_friends") + #print psyc.get_text() + for friend in psyc._query("_friends").split(" "): + print friend + print "friends:", psyc.get_text() + elif mc == "_notice_link" or mc == "_notice_unlink": + self.set_status(mc) + + def present(self, nick): + if not self.mapping.has_key(nick): + self.mapping[nick] = self.size() # where we insert it + self.insert(self.size(), nick) + + def absent(self, nick): + if self.mapping.has_key(nick): + self.delete(self.mapping[nick]) + + +from pypsyc.PSYC import parsetext, get_user +class UserGui(AbstractGui.UserGui): + def __init__(self): + AbstractGui.UserGui.__init__(self) + self.model = None + self.windows = {} + + def makewindow(self, source): + # better check it again... + if self.windows.has_key(source): + return + + win = Tkinter.Toplevel() + win.title("Dialog with " + source) + win.protocol("WM_DELETE_WINDOW", lambda: self.deletewindow(source)) + dframe = Tkinter.Frame(win) + displayfield = Tkinter.Text(dframe, width=50, height=15, + state=Tkinter.DISABLED, + wrap=Tkinter.WORD) + displayfield.grid(row=0, column=0) + scrollbar = Tkinter.Scrollbar(dframe, + command=displayfield.yview) + scrollbar.grid(row=0, column=1, sticky = Tkinter.N + Tkinter.S) + displayfield.config(yscrollcommand=scrollbar.set) + dframe.pack() + + entryfield = Tkinter.Text(win, width=54, height=5) + entryfield.pack() + frame = Tkinter.Frame(win) + entrybutton = Tkinter.Button(frame, text="send", + command=lambda: self.say(source)) + + entrybutton.grid(row = 0, column = 0) + frame.pack() + self.windows[source] = {"window" : win, + "displayfield" : displayfield, + "entryfield" : entryfield, + "button" : entrybutton} + + def deletewindow(self, source): + self.windows[source]["window"].destroy() + del self.windows[source] + + def append_text(self, displayfield, text, encoding="iso-8859-1"): + displayfield["state"] = Tkinter.NORMAL + displayfield.insert(Tkinter.END, + text.strip().decode(encoding) + '\n') + displayfield["state"] = Tkinter.DISABLED + displayfield.yview(Tkinter.END) + + def get_input(self, entryfield, encoding="iso-8859-1"): + text = entryfield.get(0.0, Tkinter.END).encode(encoding) + entryfield.delete(0.0, Tkinter.END) + return text + + def say(self, source): + self.model.set_target(source) + self.model.set_mc("_message_private") + + self.model.psyc._assign("_nick", get_user(self.model.center.ME())) + + text = self.get_input(self.windows[source]["entryfield"]) + self.model.set_text(text.strip()) + + self.append_text(self.windows[source]["displayfield"], "Du sagst: " + text) + self.model.send() + + def received(self, source, mc, mmp, psyc): + if mc == "_message_private": + source = mmp.get_source() + if not self.windows.has_key(source): + self.makewindow(source) + self.append_text(self.windows[source]["displayfield"], + get_user(source)+":"+parsetext(mmp, psyc)) + elif mc == "_internal_message_private_window_popup": + target = mmp._query("_target") + print "target:", target + if not self.windows.has_key(target): + self.makewindow(target) + + +from pypsyc.PSYC import parsetext +class RoomGui(AbstractGui.RoomGui, Tkinter.Toplevel): + def __init__(self): + Tkinter.Toplevel.__init__(self) + self.model = None + + self.buffers = {} + self.topics = {} + +## self.menu = Tkinter.Menu(self) +## self.config(menu=self.menu) + +## options = Tkinter.Menu(self.menu) +## options.add_command(label="show nicklist", +## command=self.show_nicklist) +## options.add_command(label="hide nicklist", +## command=self.hide_nicklist) +## self.menu.add_cascade(label ="Options", menu=options) + + self.topiclabel = Tkinter.Label(self, text="dummy topic") + + mainframe = Tkinter.Frame(self) + + self.textfield = Tkinter.Text(mainframe, + wrap=Tkinter.WORD) + self.textfield.config(state=Tkinter.DISABLED) + self.textfield.grid(row=0, column=0, + sticky=Tkinter.W + Tkinter.N + Tkinter.S) + + scrollbar = Tkinter.Scrollbar(mainframe, + command = self.textfield.yview) + scrollbar.grid(row=0, column=1, sticky = Tkinter.N + Tkinter.S) + + self.textfield.config(yscrollcommand=scrollbar.set) + + self.nicklist = Tkinter.Listbox(mainframe) + + entryframe = Tkinter.Frame(self) + self.entryfield = Tkinter.Entry(entryframe) + self.entryfield.grid(sticky = Tkinter.E + Tkinter.W) + + self.bleiste = Tkinter.Frame(self) + self.placebuttons = {} + + l = Tkinter.Label(self.bleiste, + text="|") + l.grid(row = 0, column = 0, sticky = Tkinter.W) + + self.topiclabel.grid(row=0, sticky = Tkinter.W) + mainframe.grid(row=1, sticky = Tkinter.W) + entryframe.grid(row=2, sticky = Tkinter.W) + self.bleiste.grid(row=3, sticky = Tkinter.W) + + self.bind("", self.say) + + def append_text(self, text, encoding="iso-8859-1"): + self.textfield["state"] = Tkinter.NORMAL + self.textfield.insert(Tkinter.END, + text.decode(encoding) + '\n') + self.textfield["state"] = Tkinter.DISABLED + self.textfield.yview(Tkinter.END) + + def received(self, source, mc, mmp, psyc): + ## evil + try: + context = mmp._query("_context").lower() +## print context + if mc.startswith("_notice_place_enter"): + ## if _source == ME eigentlich... + if not self.placebuttons.has_key(context): + self.add_room(context) + if not self.buffers.has_key(context): + self.buffers[context] = "" + self.buffers[context] += parsetext(mmp, psyc) + '\n' + if self.model.get_context() == context: + self.append_text(parsetext(mmp, psyc)) + elif mc.startswith("_notice_place_leave"): + self.buffers[context] += parsetext(mmp, psyc) + '\n' + if self.model.get_context() == context: + self.append_text(parsetext(mmp, psyc)) + elif mc == '_message_public': + text = psyc._query("_nick") + if psyc._query("_action") != "": + text += " " + psyc._query("_action") + text += ": " + parsetext(mmp, psyc) + if self.model.get_context() == context: + self.append_text(text) + self.buffers[context] += text + '\n' + elif mc == "_status_place_topic": + self.topics[mmp.get_source().lower()] = parsetext(mmp, psyc) + ## evil + except KeyError: + print "KeyError:", context + + def get_input(self, encoding="iso-8859-1"): + text = self.entryfield.get().encode(encoding) + self.entryfield.delete(0, Tkinter.END) + return text + + def say(self, event): + text = self.get_input() + if text and text[0] == '/': + # we have a command + # if we know the command, we set the appropiate mc + # else we do _request_execute + if text.startswith("/join") and text.__len__() > 16: + # 16 == len(/join psyc://h/@r) + self.model.set_mc("_request_enter") + self.model.set_target(text.split(" ")[1]) + self.model.send() + elif text.startswith("/part"): + # wie waers mit /part_logout, part_home, part_type, ... + self.model.set_mc("_request_leave") + self.model.set_target(self.model.get_context()) + self.model.send() + + elif text.startswith("/quit"): + self.model.set_mc("_request_execute") + self.model.set_target(self.model.center.ME()) + self.model.set_text("/quit") + self.model.send() + elif text.startswith("/connect"): + foo = len(text.split(" ")) + if foo == 2: + self.model.center.connect(text.split(" ")[1]) + elif foo == 3: + self.model.center.connect(text.split(" ")[1], text.split(" ")[2]) + else: + self.model.set_target(self.model.center.ME()) + self.model.set_mc("_request_execute") + self.model.set_text(text) + self.model.send() + elif text and text[0] == "#": + self.model.set_target(self.model.center.ME()) + self.model.set_mc("_request_execute") + self.model.set_text("/" + text[1:]) + self.model.send() + elif text and text[0] == "!": + self.model.set_target(self.model.get_context()) + self.model.set_mc("_request_execute") + self.model.set_text(text) + self.model.send() + elif text: +## print "msg to", self.model.get_context() + self.model.set_target(self.model.get_context()) + self.model.set_mc("_message_public") + self.model.set_text(text) + self.model.send() + + def get_topic(self, context = None): + if not context: context = self.model.get_context() + if self.topics.has_key(context): + return self.topics[context] + else: return "" + + def change_room(self, room): + # gucken ob wir nicklist hatten, u.u. loeschen + + # dieses ding sollte eigentlich ne eigene Klasse haben... + a = self.model.get_context() + if a and self.placebuttons.has_key(a): + self.placebuttons[a]["relief"] = Tkinter.RAISED + self.placebuttons[room]["relief"] = Tkinter.SUNKEN + self.model.set_context(room) + self.title("on " + room) + short = room[room.rfind("@"):] # kurzform fuer raum... nicht ideal? + self.topiclabel["text"] = short + ":" + self.get_topic() + + self.textfield.config(state=Tkinter.NORMAL) + self.textfield.delete(1.0, Tkinter.END) + self.textfield.insert(Tkinter.END, self.buffers[room]) + self.textfield.config(state=Tkinter.DISABLED) + self.textfield.yview(Tkinter.END) + + def add_room(self, room): + short = room[room.rfind("@"):] + button = Tkinter.Button(self.bleiste, text=short, + command=lambda: self.change_room(room)) + self.placebuttons[room] = button + button.grid(row=0, column = len(self.placebuttons) - 1, + sticky = Tkinter.W) + + def delete_room(self, roomname): + # delete the button? + pass + +## def hide_nicklist(self): +## self.nicklist.grid_forget() + +## def show_nicklist(self): +## self.nicklist.grid(row=0, column=2, +## sticky = Tkinter.E + Tkinter.N + Tkinter.S) + + + +from pypsyc.MMP.MMPState import MMPState +from pypsyc.PSYC.PSYCState import PSYCState +from pypsyc.MMP import _isModifier +class MainWindow(Tkinter.Tk): + def __init__(self, center = None): + Tkinter.Tk.__init__(self) + self.center = center + + self.title("pyPSYCgui - simple psyc client - see http://www.psyc.eu") + + self.menu = Tkinter.Menu(self) + self.config(menu=self.menu) + + filemenu = Tkinter.Menu(self.menu) + filemenu.add_command(label="Quit", command=self.quit) + self.menu.add_cascade(label ="File", menu=filemenu) + + connectionmenu = Tkinter.Menu(self.menu) + connectionmenu.add_command(label="connect", command=self.connect) + self.menu.add_cascade(label="Connections", menu=connectionmenu) + frame = Tkinter.Frame(self) + self.displayField = Tkinter.Text(frame, height=20, width=60, state=Tkinter.DISABLED) + self.displayField.grid(row=0, column=0) + scrollbar = Tkinter.Scrollbar + scrollbar = Tkinter.Scrollbar(frame, + command = self.displayField.yview) + scrollbar.grid(row=0, column=1, sticky = Tkinter.N + Tkinter.S) + self.displayField.config(yscrollcommand=scrollbar.set) + frame.pack() + # funzt eh net +## self.scrollbar = Tkinter.Scrollbar(self) +## self.scrollbar.pack(side=Tkinter.RIGHT, fill=Tkinter.Y) +## self.scrollbar.config(command=self.displayField.yview) + self.inputField = Tkinter.Text(self, height=5, width=60) + self.inputField.pack() + Tkinter.Button(self, text="send", command=self.input).pack( + side=Tkinter.LEFT) + + def connect(self, host = ''): + self.center.connect(host) + + def write(self, string): + self.displayField.config(state=Tkinter.NORMAL) + self.displayField.insert(Tkinter.END, string) + self.displayField.config(state = Tkinter.DISABLED) + + def input(self): +## print "TkinterGui::MainWindow::input" + cmd = self.inputField.get(1.0, Tkinter.END) + self.inputField.delete(1.0, Tkinter.END) + state = 'mmp' + mmp = MMPState() + psyc = PSYCState() + for line in cmd.split('\n'): + if line == ".": + #end of packet + break + if line == "" or (not _isModifier(line) + and state == 'mmp'): + state = 'psyc' + + if state == 'mmp': + mmp.set_state(line) + continue + if state == 'psyc': + if _isModifier(line): + psyc.set_state(line) + elif line.__len__() and line[0] == '_': + psyc.set_mc(line) + else: + psyc.append_text(line) + + self.center.send(mmp, psyc) + + +class Application(AbstractGui.MainGui): + def __init__(self, argv, center): + AbstractGui.MainGui.__init__(self, argv) + + self.mainWindow = MainWindow(center) + + ## nah... das is eigentlich auch evil ;) + sys.stdout = self.mainWindow + + def socket_check(self): + asyncore.poll(timeout=0.0) + # das hier is auch noch doof... + self.mainWindow.after(100, self.socket_check) + def run(self): + self.socket_check() + Tkinter.mainloop() + + diff --git a/GUI/Tkinter/__init__.py b/GUI/Tkinter/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/GUI/Tkinter/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/GUI/__init__.py b/GUI/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/GUI/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/GUI/__init__.pyc b/GUI/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b8ddcce3601a1a72781d30f07fff3cdeba013f9 GIT binary patch literal 122 zcmcckiI?kc#R|7%1}I8GdW=||{?>8GTY t=@(QM6jvteyN7z}$H!;pWtPOp>lIX%Z~)cW8GdW=||{?>8GTY w=@(QM6jvteyN7z}msjY=$7kkcmc+;F6;zgR0F~P0=BJeAq}qWDECyl*0EcH7^8f$< literal 0 HcmV?d00001 diff --git a/GUI/wx/devGui.py b/GUI/wx/devGui.py new file mode 100644 index 0000000..4e46bcf --- /dev/null +++ b/GUI/wx/devGui.py @@ -0,0 +1,696 @@ +# be warned this is the _dev_ version of the wx gui +# perhaps it will work, perhaps it's all borked up, +# this is a plazground shouldn't be used on a day to +# day basis +import GUI.Abstract.Gui as AbstractGui +import asyncore, sys, os +import ConfigParser +from string import split, rfind, strip + +from pypsyc.PSYC.PSYCRoom import PSYCPackage +from pypsyc.PSYC.PSYCRoom import Friends as FriendsPackage +from pypsyc.PSYC import parsetext, get_user + +from wxPython.wx import * +from wxPython.stc import * + + +from wxPython.lib.rcsizer import RowColSizer +from wxPython.lib.grids import wxFlexGridSizer + +VERSION = 'pyPSYC-wx 0.0.1' + +# for linux/posix this should work +CONFIG_FILE = os.getenv("HOME") + "/.pypsyc/wx-config" + +# windows users should uncomment the next line and comment out the line above +# CONFIG_FILE = 'wx-config' + +ID_ABOUT = 1002 +ID_CONNECT = 1001 +ID_DISCONNECT = 10011 +ID_SAY = 3300 +ID_NOTEBOOK = 3301 +ID_STATUS = 9901 +ID_MENU = 9902 +ID_BUDDY_LIST = 9903 +ID_BUDDY_LIST_DK = 990301 +ID_EXIT = 1099 +ID_CLOSECHATNOTEBOOK = 2099 +ID_CLOSECHATNOTE = 2098 + +def opj(path): + """Convert paths to the platform-specific separator""" + return apply(os.path.join, tuple(path.split('/'))) + +class Tab(wxPanel): + def __init__(self, context, notebook): + self.notebook = notebook + wxPanel.__init__(self, self.notebook, -1) + #panel = wxPanel(self.notebook, -1) + button = wxButton(self, ID_CLOSECHATNOTE, "close chat") + #button2 = wxButton(self, ID_STATUS, "NOOP") + self.nick_box = wxTextCtrl(self, -1, style=wxTE_READONLY) + box = wxBoxSizer(wxHORIZONTAL) + box.Add(self.nick_box, 1,wxEXPAND) + #box.Add(button2,0,wxEXPAND) + box.Add(button,0,wxEXPAND) + + #scroller = wxScrolledWindow(self, -1, size=(-1,-1)) + self.text_box = wxTextCtrl(self, -1, style=wxTE_MULTILINE|wxTE_RICH2|wxTE_READONLY|wxTE_AUTO_URL, size=wxDefaultSize) + self.entry_box = wxTextCtrl(self, ID_SAY, style=wxTE_PROCESS_ENTER|wxTE_PROCESS_TAB, size=wxDefaultSize) + + sizer = RowColSizer() + sizer.Add(box, pos=(1,1), colspan=3,flag=wxEXPAND) + sizer.Add(self.text_box, pos=(2,1),flag=wxEXPAND, colspan=3) + #sizer.Add(scroller, pos=(2,1),flag=wxEXPAND, colspan=3) + sizer.Add(self.entry_box, pos=(3,1),flag=wxEXPAND, colspan=2) + sizer.AddGrowableCol(1) + sizer.AddGrowableRow(2) + self.SetSizer(sizer) + + def append_text(self, text): + #text = text.decode('iso-8859-1') + if os.name == 'posix': + #self.text_box.SetEditable(True) + #ende = self.text_box.GetInsertionPoint() + #self.text_box.ShowPosition(ende+len(text)) + self.text_box.AppendText(text) + #self.text_box.SetInsertionPointEnd() + #ende = self.text_box.GetInsertionPoint() + #self.text_box.ShowPosition(ende+len(text)) + self.text_box.ScrollLines(2) + #self.text_box.SetEditable(False) + else: + self.text_box.WriteText(text) + self.text_box.ScrollLines(2) + + def clear_entry(self): + self.entry_box.Clear() + self.entry_box.SetInsertionPoint(0) + + def append_entry(self, text): + self.entry_box.AppendText(text) + + def set_topic(self, text): + self.nick_box.Clear() + self.nick_box.AppendText(text) + + def get_text(self): + return self.entry_box.GetValue() + +class FriendList(wxFrame, PSYCPackage): + def __init__(self, parent=None, ID=0, title='pyPSYC', center=None, config=None, pos=wxDefaultPosition, size=wxSize(190, 400), style=wxDEFAULT_FRAME_STYLE): + # bei mir is friendlist, firendlist und maingui in einem + # zumindestens sollte es das sein ob es so klappt weiss ich noch nicht + self.global_config = config + self.config = ConfigParser.ConfigParser() + self.config.read(CONFIG_FILE) + + if self.config: + pos = split(self.config.get('gui', 'wx_friend_pos'), ',') + size = split(self.config.get('gui', 'wx_friend_size'), ',') + + wxFrame.__init__(self, parent, ID, title, wxPoint(int(pos[0]), int(pos[1])) , wxSize(int(size[0]), int(size[1])), style) + PSYCPackage.__init__(self) + self.currentBuddy = None + self.center = center + self.model = None + self.packagename = "Friends gui" + # we have to register by ourselves + # perhaps it's not the way to do it but it works +# no.. it's not working.. --lynX +# self.center.register(FriendsPackage(self)) + + # menubar, statusbar et al + self.CreateStatusBar() + self.SetStatusText("welcome to pyPSYC") + menu = wxMenu() + menu.Append(ID_CONNECT, "&Connect", "connect to the net") + menu.Append(ID_ABOUT, "&About", "tritratrullala") + menu.AppendSeparator() + menu.Append(ID_EXIT, "&Exit", "leave us") + menuBar = wxMenuBar() + menuBar.Append(menu, "&File"); + self.SetMenuBar(menuBar) + + ##'buddy' list, perhaps ;]] + self.BuddyList = wxListCtrl(self, ID_BUDDY_LIST, style=wxLC_REPORT|wxLC_SINGLE_SEL|wxSUNKEN_BORDER) + self.BuddyList.InsertColumn(0, "ST") + self.BuddyList.InsertColumn(1, "Nick")# , wxLIST_FORMAT_RIGHT) + self.BuddyList.SetColumnWidth(0, 20) + ##end buddy list + + # define the buttons and so on at the bottom of the window + self.status = wxComboBox(self, ID_STATUS, "", choices=["", "This", "is a", "Place", "to put commands"], size=(150,-1), style=wxCB_DROPDOWN) + self.menu_button = wxButton( self, ID_MENU, 'pyPSYC') + self.exit_button = wxButton( self, ID_EXIT, 'exit') + self.con_menu = wxBoxSizer(wxHORIZONTAL) + self.con_menu.Add(self.menu_button, 1, wxALIGN_BOTTOM) + self.con_menu.Add(self.exit_button, 1, wxALIGN_BOTTOM) + + sizer = wxFlexGridSizer(3, 0 , 0,0) + sizer.Add(self.BuddyList, 1, wxGROW) + sizer.Add(self.con_menu, 1,wxGROW) + sizer.Add(self.status, 1,wxGROW) + sizer.AddGrowableRow(0) + sizer.AddGrowableCol(0) + # do something so that the buttons don't vanish in a too small window + # this is h4x0r-style but does the job at the moment + sizer.SetItemMinSize(self.BuddyList, 30, 10) + sizer.SetMinSize(wxSize(200,280)) + self.SetSizer(sizer) + self.SetAutoLayout(true) + + # event handling + EVT_BUTTON( self, ID_EXIT, self.onExit ) + EVT_BUTTON( self, ID_MENU, self.doConnect ) + #EVT_BUTTON(self, ID_CONNECT, self.doConnect) + #EVT_BUTTON(self, ID_CLOSECHATNOTE, self.closeChat) + EVT_MENU( self, ID_EXIT, self.onExit) + EVT_MENU(self, ID_CONNECT, self.doConnect) + #EVT_LEFT_DCLICK(self.BuddyList, self.onFriend) + #EVT_LIST_ITEM_SELECTED(self, ID_BUDDY_LIST, self.OnBuddySelected) + # ect_activated muss weg sonst wird zweimal nen chat geoeffnet ;]] + ##EVT_LIST_ITEM_ACTIVATED(self, ID_BUDDY_LIST, self.onFriend) + + def received(self, source, mc, mmp, psyc): + # muss ueberall sein, warum weiss ich noch nicht so genau + # .._present .._absent versteh ich also is es da;] + if mc == "_notice_friend_present": + self.present(mmp.get_source()) + elif mc == "_notice_friend_absent": + self.absent(mmp.get_source()) + elif mc == "_notice_link" or mc == "_notice_unlink" or mc == "_status_linked": + self.set_status(mc) + + def set_model(self, model): + self.model = model + + def present(self, nick): + """ do what has to be done if a friend turns up """ + # at the moment: + pass + #fuer das x muesste man wissen wie lang die buddy list bisher ist + #sprich vllt doch wieder nen dict dafuer bauen or so + #self.BuddyList.InsertStringItem(x ,'on') + #self.BuddyList.SetStringItem(x, 1, nick) + + def absent(self, nick): + """ do what has to be done if a friend leaves us """ + # at the moment also: + pass + + def set_status(self, status): + """ we should tell the user if the status changes """ + #print 'link status: ' + str(status) + + def OnBuddySelected(self, event): + self.currentBuddy = event.m_itemIndex + event.Skip() + + def doConnect(self, event, host = ''): + self.center.connect(host) + #print 'connecting...' + event.Skip() + + def onExit(self, event): + """ do stuff we have to do before exiting """ + #print 'exiting...' + pos = str(self.GetPositionTuple()[0]) + ', ' + str(self.GetPositionTuple()[1]) + size = str(self.GetSizeTuple()[0]) + ', ' + str(self.GetSizeTuple()[1]) + self.config.set('gui', 'wx_friend_pos', pos) + self.config.set('gui', 'wx_friend_size', size) + config_file = file(CONFIG_FILE, 'w') + self.config.write(config_file) + config_file.close() + + self.model.set_mc("_request_execute") + self.model.set_target(self.model.center.ME()) + self.model.set_text("/quit") + self.model.send() + self.Close(TRUE) + #sys.exit(0) + event.Skip() + +class UeberGui(wxFrame): + def __init__(self, parent=NULL, ID=-1, title='uebergui teil', status_tab = 1, pos=wxDefaultPosition, size=wxDefaultSize, style=wxDEFAULT_FRAME_STYLE): + self.config = ConfigParser.ConfigParser() + self.config.read(CONFIG_FILE) + + if self.config: + pos = split(self.config.get('gui', 'wx_room_pos'), ',') + size = split(self.config.get('gui', 'wx_room_size'), ',') + wxFrame.__init__(self, parent, ID, title, wxPoint(int(pos[0]), int(pos[1])) , wxSize(int(size[0]), int(size[1])), style) + #PSYCPackage.__init__(self) + self.notebook = wxNotebook(self, ID_NOTEBOOK) + + self.tabs = [] # ein nummer zu context mapping + self.querys = [] + self.buffers = {} # context : "chatter going on in this context" + self.tab_inst = {} # context : tab instanz + self.query_inst = {} # context : tab instanz + self.members = {} # dictionary a la { 'PSYC' : ['fippo', 'lethe', ...]} + + if status_tab == 1: + self.addTab('status') + EVT_TEXT_ENTER(self, ID_SAY, self.say) + EVT_NOTEBOOK_PAGE_CHANGED(self, ID_NOTEBOOK, self.OnTabChange) + EVT_CLOSE(self, self.onClose ) + EVT_TEXT(self, ID_SAY, self.EvtText) + + def EvtText(self, event): + #print 'EvtText: %s' % event.GetString() + event.Skip() + + def find_nick(self, wiesel): + positve_members= [] + members = self.members[self.model.get_context()] + for member in members: + member = strip(member) + if member.startswith(wiesel): + positve_members.append(member) + return positve_members + + def EvtChar(self, event): + #print 'EvtChar: ' + str(event.GetKeyCode()) + if event.GetKeyCode() == 9: + # hier muessen wir nick raten und so weiter + wiesel = self.tab_inst[self.model.get_context()].get_text() + wiesel2 = wiesel + wiesel = split(wiesel, ' ') + marder = self.find_nick(wiesel[-1]) + if len(marder) > 1: + print 't1' + self.tab_inst[self.model.get_context()].append_text(str(marder) + '\n') + elif len(marder) == 1: + print 't2' + nick = marder[0] + text = wiesel2 + pos = - len(wiesel[-1]) + text = text[:pos] + nick + self.tab_inst[self.model.get_context()].clear_entry() + self.tab_inst[self.model.get_context()].append_entry(text) + else: + print 't3' + else: + # hier passen wir einfach und reset'en den coutner + self.dep_count = 0 + event.Skip() + + def onClose(self,event): + print 'UeberGui.onCLose() worked' + + def say(self, event): + """ wird gerufen wenn jmd enter drückt """ + text = event.GetString() + #text = text.encode('iso-8859-1') + if text != '' and text[0] == '/': + # we have a command + # if we know the command, we set the appropiate mc + # else we do _request_execute + if text.startswith("/join") and text.__len__() > 16: + # 16 == len(/join psyc://h/@r) + self.model.set_mc("_request_enter") + self.model.set_target(text.split(" ")[1]) + self.model.send() + elif text.startswith("/part"): + # wie waers mit /part_logout, part_home, part_type, ... + self.model.set_mc("_request_leave") + self.model.set_target(self.model.get_context()) + self.model.send() + + elif text.startswith("/quit"): + self.model.set_mc("_request_execute") + self.model.set_target(self.model.center.ME()) + self.model.set_text("/quit") + self.model.send() + + elif text.startswith('/me') and text[3] == ' ': + self.model.set_target(self.model.get_context()) + self.model.set_mc('_message_public') + self.model.set_psycvar('_action', text[4:]) + self.model.set_text('') + self.model.send() + + elif text.startswith('/names'): + self.model.set_target(self.model.get_context()) + self.model.set_mc('_request_members') + self.model.set_text('') + self.model.send() + + #elif text.startswith('/lnames'): + # context = self.model.get_context() + # self.tab_inst[context].append_text(self.members[context][0] + '\n') + + elif text.startswith("/connect"): + foo = len(text.split(" ")) + if foo == 2: + self.model.center.connect(text.split(" ")[1]) + elif foo == 3: + self.model.center.connect(text.split(" ")[1], text.split(" ")[2]) + else: + self.model.set_target(self.model.center.ME()) + self.model.set_mc("_request_execute") + self.model.set_text(text) + self.model.send() + elif text != '' and text[0] == "#": + self.model.set_target(self.model.center.ME()) + self.model.set_mc("_request_execute") + self.model.set_text("/" + text[1:]) + self.model.send() + elif text != '' and text[0] == "!": + self.model.set_target(self.model.get_context()) + self.model.set_mc("_request_execute") + self.model.set_text(text) + self.model.send() + elif text != '' and text[-1] == '?': + self.model.set_target(self.model.get_context()) + self.model.set_mc("_message_public") + self.model.set_text(text) + self.model.send() + elif text != '': + #print "msg to", self.model.get_context() + self.model.set_target(self.model.get_context()) + self.model.set_mc("_message_public") + self.model.set_text(text) + self.model.set_psycvar('_action', 'testet') + self.model.send() + self.tab_inst[self.model.context].clear_entry() + event.Skip() + + def addTab(self, title): + panel = Tab(title, self.notebook) + if title != 'status': + short_title= title[rfind(title, '@'):] + self.model.set_context(title) + panel.append_text('heading over to ' + title + '\n') + self.model.set_target(self.model.get_context()) + self.model.set_mc("_request_members") + self.model.set_text('') + self.model.send() + + else: + short_title = title + self.notebook.AddPage(panel, short_title, select = 1) + + self.tab_inst[title] = panel + self.SetTitle(title) + EVT_CHAR(panel.entry_box, self.EvtChar) + + if title == 'status': + motd = 'Welcome to ' + VERSION + '\n for Q&A go to psyc://ve.symlynx.com/@PSYC \n or contact tim@trash-media.de for GUI problems \n for all the other problems go and talk to fippo!' + panel.append_text(motd) + + self.tabs.append(title) + #self.buffers[title] = '' + + def addQuery(self, title): + panel = Tab(title, self.notebook) + if title != 'status': + short_title= title[rfind(title, '~'):] + #self.model.set_context(title) + print dir(self.model) + panel.append_text('talking to ' + short_title + '\n') + #self.model.set_target(self.model.get_context()) + #self.model.set_mc("_request_members") + #self.model.set_text('') + #self.model.send() + else: + short_title = title + self.notebook.AddPage(panel, short_title, select = 1) + + self.query_inst[title] = panel + self.SetTitle(title) + if title == 'status': + motd = 'Welcome to ' + VERSION + '\n for Q&A go to psyc://ve.symlynx.com/@PSYC \n or contact tim@trash-media.de for GUI problems \n for all the other problems go and talk to fippo!' + panel.append_text(motd) + + self.querys.append(title) + self.source = self.querys[-1] + #print (self.querys) + #print '<<<<<<<<<<<<<<' + #self.buffers[title] = '' + + def OnTabChange(self, event): + old = event.GetOldSelection() + new = event.GetSelection() + self.SetTitle(self.tabs[new]) + self.model.set_context(self.tabs[new]) + #if self.buffers[self.model.context] != '': + # self.tab_inst[self.model.context].append_text(self.buffers[self.model.context]) + # self.buffers[self.model.context] = '' + event.Skip() + + def msg(self, text): + """ mehr text! """ + +class UserGui(UeberGui, AbstractGui.UserGui, PSYCPackage): + def __init__(self): + """ das hier is fuer querys """ + PSYCPackage.__init__(self) + UeberGui.__init__(self, status_tab = 0) + self.model = None + self.source = '' + #pass + + def say(self, event): + text = event.GetString() + #source self.tabs[] + source = self.source + self.model.set_target(source) + self.model.set_mc("_message_private") + self.model.psyc._assign("_nick", get_user(self.model.center.ME())) + + #text = self.get_input(self.windows[source]["entryfield"]) + self.model.set_text(text.strip()) + #self.append_text(self.windows[source]["displayfield"], "Du sagst: " + text) + self.model.send() + self.query_inst[source].append_text('Du> ' + text + '\n') + self.query_inst[source].clear_entry() + event.Skip() + + + def received(self, source, mc, mmp, psyc): + context = 'status' + if mmp._query("_source") != None: + context = mmp._query("_source").lower() + else: + context = source.lower() + + if mc == "_internal_message_private_window_popup": + # open a new tab + self.Show(True) + self.addQuery('test') + + elif mc == "_message_echo_private": + self.Show(True) + text = '' + text += 'Du>' + if context not in self.querys: + self.addQuery(context) + text += ' ' + parsetext(mmp, psyc) + self.query_inst[context].append_text(text + '\n') + + elif mc =='_message_private': + self.Show(True) + text = '' + text += 'Anderer_Nick>' + if context not in self.querys: + self.addQuery(context) + text += ' ' + parsetext(mmp, psyc) + self.query_inst[context].append_text(text + '\n') + + def set_model(self, model): + self.model = model + + def OnTabChange(self, event): + old = event.GetOldSelection() + new = event.GetSelection() + self.SetTitle(self.querys[new]) + self.source = self.querys[new] + #self.model.set_context(self.tabs[new]) + #if self.buffers[self.model.context] != '': + # self.tab_inst[self.model.context].append_text(self.buffers[self.model.context]) + # self.buffers[self.model.context] = '' + event.Skip() + + + +class RoomGui(UeberGui, AbstractGui.RoomGui, PSYCPackage): + def __init___(self): + """ das hier is fuer raeume """ + PSYCPackage.__init__(self) + UeberGui.__init__(self,pos=pos, size=size) + self.model = None + + EVT_CLOSE(self, self.onClose) + + def onClose(self,event): + #print 'RoomGui.onClose() worked' + pos = str(self.GetPositionTuple()[0]) + ', ' + str(self.GetPositionTuple()[1]) + size = str(self.GetSizeTuple()[0]) + ', ' + str(self.GetSizeTuple()[1]) + self.config.set('gui', 'wx_room_pos', pos) + self.config.set('gui', 'wx_room_size', size) + config_file = file(CONFIG_FILE, 'w') + self.config.write(config_file) + config_file.close() + #self.Show(False) + event.Skip() + + def set_model(self, model): + self.model = model + + def received(self, source, mc, mmp, psyc): + context = 'status' + if mmp._query("_context") != None: + context = mmp._query("_context").lower() + else: + context = source.lower() + #if psyc._query('_nick_place') != None: + # context = 'psyc://ve.symlynx.com/@' + psyc._query('_nick_place').lower() + #context = source.lower() + if mc.startswith('_notice_place_leave'): + if context not in self.tabs: + self.addTab(context) + text = parsetext(mmp, psyc) + self.tab_inst[context].append_text(text + '\n') + if self.members.has_key(context): + nick = psyc._query('_nick') + self.members[context].remove(nick) + + if mc.startswith('_notice_place_enter'): + self.Show(true) + if context not in self.tabs: + self.addTab(context) + text = parsetext(mmp, psyc) + self.tab_inst[context].append_text(text + '\n') + if self.members.has_key(context): + nick = psyc._query('_nick') + if nick not in self.members[context]: + self.members[context].append(nick) + + elif mc == '_message_public_question': + text = '' + text += str(psyc._query("_nick")) + if context not in self.tabs: + self.addTab(context) + text += ' fragt ' + parsetext(mmp, psyc) + self.tab_inst[context].append_text(text + '\n') + + elif mc.startswith('_status_place_topic') or mc.startswith('_notice_place_topic'): + topic = psyc._query('_topic') + context = source.lower() + if context not in self.tabs: + self.addTab(context) + self.tab_inst[context].set_topic(topic) + + elif mc == '_message_public': + text = '' + text += str(psyc._query("_nick")) + if context not in self.tabs: + self.addTab(context) + if psyc._query("_action"): + ptext = parsetext(mmp, psyc) + if ptext == '': + text += " " + psyc._query("_action") + else: + text += " " + psyc._query("_action") + '> ' + ptext + else: + text += "> " + parsetext(mmp, psyc) + #text = text.encode('iso-8859-1') + self.tab_inst[context].append_text(text + '\n') + #print '!!' + context + ': ' + text + + elif mc == '_status_place_members': + text = parsetext(mmp, psyc) + if context not in self.tabs: + self.addTab(context) + members = split(psyc._query('_members'), ', ') + self.members[context] = members + self.tab_inst[context].append_text(text + '\n') + + else: + # everything we don't know goes into the status tab + # hopefully parsetext doesn't crash with' bogus' pakets + text = source.lower() + ' >>> ' + text += parsetext(mmp, psyc) + self.tab_inst['status'].append_text(text + '\n') + + + +class MainWindow(PSYCPackage): + # eigentlich brauch das kein schwein oder??? + def __init__(self, argv): + """ was ich hiermit mache weiss ich noch net genau, eigentlich brauch + ich es net im moment lebt es für den devel handler""" + self.center = None + PSYCPackage.__init__(self) + + def title(self, text): + pass + + def run(self): + pass + + def quit(self): + self.center.quit() + sys.exit(0) + + def connect(self): + pass + + def write(self, text): + #print text + pass + +class MySplashScreen(wxSplashScreen): + def __init__(self, argv, center, config): + self.center = center + self.argv = argv + self.config = config + bmp = wxImage(opj("GUI/wx/psych2o.jpg")).ConvertToBitmap() + wxSplashScreen.__init__(self, bmp, + wxSPLASH_CENTRE_ON_SCREEN|wxSPLASH_TIMEOUT, + 4000, None, -1, + style = wxFRAME_NO_TASKBAR|wxSTAY_ON_TOP) + EVT_CLOSE(self, self.OnClose) + + def OnClose(self, evt): + frame = FriendList(NULL, -1, "pyPSYC 0.0.0.0.0.1", center=self.center, config=self.config) + frame.Show() + evt.Skip() + +class Application(wxApp): + def __init__(self, argv, center, config): + self.center = center + self.argv = argv + self.config = config + wxApp.__init__(self,0) + #print 'tt' + + def OnInit(self): + #self.frame = FriendList(NULL, -1, "pyPSYC 0.0.0.0.0.1", center=self.center, config=self.config) + #self.frame.Show(true) + #sys.stdout = self.frame + wxInitAllImageHandlers() + splash = MySplashScreen(self.argv, self.center, self.config) + self.SetTopWindow(splash) + splash.Show() + return True +## +## self.timer = wxPyTimer(self.socket_check) +## self.timer.Start(100) # alle 100 ms +## #print 'ttt' +## +## def socket_check(self): +## asyncore.poll(timeout=0.0) +## + def run(self): + # blah mainloop + #print 'tttt' + + from twisted.internet import wxsupport, reactor + wxsupport.install(self) + print "running reactor..." + reactor.run() diff --git a/GUI/wx/devGui.pyc b/GUI/wx/devGui.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbb9d4b3a1c073a38902e75857186fedf1e6bb4f GIT binary patch literal 22048 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 G!2bg~ov*|I literal 0 HcmV?d00001 diff --git a/GUI/wx/psych2o.jpg b/GUI/wx/psych2o.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48149711716d8898406d6a9dfaa0d021b916f757 GIT binary patch literal 80224 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

a^YF(ox1KF>*H(DDxey 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 G$^QdL<1JMH literal 0 HcmV?d00001 diff --git a/GUI/wx/test.py b/GUI/wx/test.py new file mode 100644 index 0000000..6f08766 --- /dev/null +++ b/GUI/wx/test.py @@ -0,0 +1,7 @@ +## some sort of testing thingie +## damit man es sieht wies geht udn die Gui.py auch sonst importieren kann + +import Gui + +app = Gui.Application(0) +app.MainLoop() diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..5daab9f --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,24 @@ +pyPSYC depends on python (tested with 2.2.1) and tkinter +(note: if you want you can write qt, gtk, wxpython gui's etc) +also, this version depends on twisted python from twistedmatrix.com + +prior to starting you need to edit the config-example file and move it +to $HOME/.pypsyc/config + +on windows keep it in the same directory as chat_client.py and change +line 22 + config.read(os.getenv("HOME") + "/.pypsyc/config") +to + config.read("config") +This will soon be automated somehow. +start pyPSYC with `python chat_client.py` + +there are two command styles... local command which start with "/" +currently there are two of them, "/join psyc://host/@room" +(atm this works only if you are connected to host) +and "/part" which parts the current room. +/quit is also supported, although atm it is semantically an alias for #quit + +the other command style is commands starting with "#" +they are handled by your UNI with a _request_execute + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..08041c7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ + + pyPSYC, a multiplexing PSYC client written in Python + Copyright (C) 2002, 2003 by Philipp "fippo" Hancke + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + 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. + + Taken from lynx license for the psyced: + + SPECIAL ENCOURAGEMENTS FOR PSYC DEVELOPERS. + + 1. Whenever you make derivates of this library publically available, be it + in source or binary form, please keep all PSYC network transactions + according to the current specification of the PSYC protocol as available + from http://www.psyc.eu. This will help keeping PSYC implementations + interoperable. + + 2. Whenever you release a derivative work of this software, please inform + the PSYC community of the existence of your new software by promoting it + on http://about.psyc.eu. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bcaaec8 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +# toplevel makefile +clean : + find . -name "*~" | xargs rm + find . -name "*.pyc" | xargs rm diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..65d6e4c --- /dev/null +++ b/README.txt @@ -0,0 +1,6 @@ +Obtain the newest version by git... + + git clone git://git.psyced.org/pypsyc + +See also http://about.psyc.eu/pypsyc + diff --git a/README.wx b/README.wx new file mode 100644 index 0000000..e75915d --- /dev/null +++ b/README.wx @@ -0,0 +1,55 @@ +WELCOME TO pyPSYC(with a funky wxWindows GUI) + +bugs & black-mail to tim@rash-media.de + +-> HOWTO TURN THE BEAST ALIVE + + * install wxPython from www.wxpython.org + + * extract the tar + + * edit config-example and put it into ~/.pypsyc/config + + * edit wx-config-example and put it into ~/.pypsyc/wx-config only copying it + there should be enough and you shouldn't edit it afterwards, it's only a + palce for the gui to remeber things like size, position and so on + + * windows users should only rename the two files, instead of moving them: + + config-example -> config + wx-config-example -> wx-config + + * windows users have to edit chat_client.py to make sure it looks for the + config file in the right place and you have to edit the GUI/wx/devGui.py + file also. + + * you now should be setup and ready to explore the world of psyc + + +-> OFFICIAL NOTES + + * you need wxPython 2.4.1 and Python 2.2 other versions might also work but + who knows + + +-> INOFFICIAL NOTES + + * versions of wxPython <2.4.1 have a nasty bug that prevents the msg window + from scrolling so you should upgrade + + * we have a nick completer now!!! shout out loud: "Hurraayy!" + + * bribe marder if you want a shiny-one-click windows installer + + * if you don't like the default speak action look into devGui.yp and change + if, shoudl be quite easy + + +-> STATUS OF THE BEAST + + * there are reports that pyPSYC crashes with win2k, we are looking into that + + * querys don't work + + * you can't close the tabs ;]] + diff --git a/config-example b/config-example new file mode 100644 index 0000000..d2cf8e7 --- /dev/null +++ b/config-example @@ -0,0 +1,15 @@ +# should be changed to support http://www.psyc.eu/storage + +[main] +uni: psyc://host/~user +password: geheim + +[packages] +friends: enabled +conferencing: disabled +biff: disabled +user: disabled +devel: enabled + +[gui] +type: wx diff --git a/console-client.py b/console-client.py new file mode 100755 index 0000000..b6d8156 --- /dev/null +++ b/console-client.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python + +# +# broken - fix this some +# +import asyncore +import ConfigParser +import os +import sys +# here we start importing our own modules +from pypsyc.PSYC.PSYCMessagecenter import PSYCMessagecenter +from pypsyc.PSYC import parsetext +# import packages +from pypsyc.PSYC.PSYCRoom import Authentication as AuthenticationPackage +#import Listener + +debug=0 + +class stdin_channel (asyncore.file_dispatcher): + def __init__(self): + asyncore.file_dispatcher.__init__(self, 0) + self.buffer = "" + def handle_read(self): + data = self.recv(512) + self.buffer += data + lines = self.buffer.split("\n") + for line in lines[:-1]: + self.lineReceived(line) + self.buffer = lines[-1] + def handle_close(self): + try: + self.close() + except: + pass + def lineReceived(self, line): + print "line:", line # overridden + def writable(self): + return 0 + def log(self, *ignore): + pass + +from pypsyc.PSYC.PSYCRoom import PSYCPackage +class stdin_client(stdin_channel, PSYCPackage): + def __init__(self): + stdin_channel.__init__(self) + PSYCPackage.__init__(self) + self.methods = ["devel"] + self.packagename = "pyPSYC console client" + def received(self, source, mc, mmp, psyc): + if debug: + #print self.packagename, "handling", mc, "from", source + print "" + print "mc:", mc + for (key, val) in mmp.thisState.items(): + print "MMP:", key, "=>", val + for (key, val) in psyc.thisState.items(): + print "PSYC:", key, "=>", val + print parsetext(mmp, psyc) + if mc == "_echo_logoff" or mc == "_status_unlinked": + print "Bye bye." + # eh? shouldn't this clean up and exit? --lynX + sys.exit() + + def set_mc(self, mc): self.psyc.set_mc(mc) + def set_target(self, target): self.mmp._assign("_target", target) + + def set_text(self, text): + self.psyc.reset_text() + self.psyc.append_text(text) + + def send(self): + self.center.send(self.mmp, self.psyc) + # this shouldn't need to be done here + self.mmp.reset_state() + self.psyc.reset_state() + # after each send the temporary vars must be reset + # to the permanent var state automatically. -lynX + def lineReceived(self, line): + self.mmp._set("_source_identification", self.center.ME()) + self.set_target(self.center.ME()) + self.set_mc("_request_input") + self.set_text(line) + self.send() + + +config = ConfigParser.ConfigParser() +config.read(os.getenv("HOME") + "/.pypsyc/config") + +console = stdin_client() + +center = PSYCMessagecenter(config) + +center.register(AuthenticationPackage(config)) +center.register(console) + +center.connect() + +asyncore.loop() + diff --git a/fippos-twisted/README b/fippos-twisted/README new file mode 100644 index 0000000..e214c2f --- /dev/null +++ b/fippos-twisted/README @@ -0,0 +1,24 @@ +pyPSYC - asyncore based library for PSYC + +The application programming interface (API) of this library is stable, +says fippo. But he left it unmaintained since 2005. + + +News at http://about.psyc.eu/pypsyc +Download by anonCVS from the same server as psyced. + :pserver:anonymous@cvs.psyced.org:/CVS/anoCVS + + +QUESTIONS? +/tell psyc://psyced.org/~fippo or come to psyc://psyced.org/@welcome + +AUTHORS + Philipp Hancke + + + Tim Head + + + Andreas Neue + + diff --git a/fippos-twisted/contrib/ircd.py b/fippos-twisted/contrib/ircd.py new file mode 100644 index 0000000..3916b68 --- /dev/null +++ b/fippos-twisted/contrib/ircd.py @@ -0,0 +1,230 @@ +# usage: twistd -noy ircd.py + +import sys +from twisted.application import service, internet +from pypsyc.center import ServerCenter +from pypsyc.net import PSYCServerFactory +from pypsyc.objects import PSYCReceiver +from pypsyc.objects.server import Person, Place, GroupSlave +from pypsyc import parseURL + +from twisted.protocols import irc +from twisted.internet.protocol import ServerFactory + + +""" +watch out, the main purpose of this thing is to debug the context slave +system +""" + +class IRCD(irc.IRC, PSYCReceiver): + center = None + password = '' + nick = '' + url = '' + source = None + def __init__(self, center, hostname): + self.center = center + self.hostname = hostname + def connectionMade(self): + peer = self.transport.getPeer() + self.source = 'object:%s:%d'%(peer.host, peer.port) + self.center.register_object(self.source, self) + def connectionLost(self, reason): + if self.url: + self.irc_QUIT(None, None) + elif self.nick is '': + print 'closing unknown connection' + def sendNumeric(self, numeric, msg): + self.sendLine(':%s %s %s :%s'%(self.hostname, numeric, self.nick, msg)) +# def sendLine(self, line): +# irc.IRC.sendLine(self, line.encode('iso-8859-1')) + def sendMessage(self, source, target, cmd, data = None): + s = ':%s %s %s'%(self.url2prefix(source), target, cmd) + if data: + s += ' :%s'%data + self.sendLine(s) + def sNotice(self, data): + self.sendLine(':%s NOTICE %s :%s'%(self.hostname, self.nick, data)) + """helper functions""" + def expandUrl(self, target): + """quick and dirty is_uniform check""" + if target.find(':') is -1: + if target[0] == '#': + return self.hostname + '/@%s'%target[1:] + return self.hostname + '/~%s'%target + if target[0] == '#': + return target[1:] + return target + def minimizeUrl(self, source): + if source.startswith(self.hostname): + # remark: +2 to skip trailing /@ + return source[len(self.hostname) + 2:] + return source + def url2prefix(self, url): + if url.find(':') != -1: + u = parseURL(url) + if u['resource'][0] == '~': + ident = u['resource'][1:] + else: + ident = '*' + host = u['host'] + else: + ident = url # its a local nick + host = 'localuser' + return '%s!%s@%s'%(url, ident, host) + + """irc_ command hooks""" + def irc_USER(self, prefix, params): + pass + def irc_PASS(self, prefix, params): + self.password = params[0] + def irc_NICK(self, prefix, params): + self.nick = params[0] + self.center.msg({'_source' : self.source, + '_target' : self.expandUrl(self.nick), + '_password' : self.password}, + '_request_link', '') + def irc_PRIVMSG(self, prefix, params): + target = params[0] + mc = '_message_private' + if target[0] == '#': + mc = '_message_public' + self.center.msg({ '_source' : self.source, + '_target' : self.expandUrl(target), + '_nick' : self.nick}, + mc, params[-1]) + def irc_JOIN(self, prefix, params): + chan = params[0] + self.center.msg({ '_source' : self.source, + '_target' : self.expandUrl(chan), + '_nick' : self.nick}, + '_request_enter', '') + def irc_PART(self, prefix, params): + chan = params[0] + self.center.msg({ '_source' : self.source, + '_target' : self.expandUrl(chan), + '_nick' : self.nick}, + '_request_leave', '') + def irc_QUIT(self, prefix, params): + self.center.msg({ '_source' : self.source, + '_target' : self.url }, + '_request_unlink', '') + def irc_unknown(self, prefix, command, params): + if command == 'ROSTER': + self.center.msg({ '_source' : self.source, + '_target' : self.url }, + '_request_roster', '') + elif command == 'FRIEND': + self.center.msg({ '_source' : self.source, + '_target' : self.expandUrl(params[0]) }, + '_request_friendship', 'Lass uns Freunde sein') + else: + print 'unknown irc cmd %s'%command + """pypsyc msg API""" + def msgUnknownMethod(self, vars, mc, data): + print 'unsupported %s from %s'%(mc, vars['_source']) + def msg_notice_link(self, vars, mc, data): + self.url = vars['_source'] + self.sendNumeric(irc.RPL_WELCOME, 'Hello, %s'%self.nick) + self.sendNumeric(irc.RPL_YOURHOST, 'Welcome to %s'%self.hostname) + self.sendNumeric(irc.RPL_MYINFO, '%s is a pyPSYC daemon IRC interface'%self.hostname) + def msg_message_private(self, vars, mc, data): + if vars['_target'] is self.source: + t = self.nick + else: # should not happen + raise + # TODO: might be appropriate to use self.privmsg() + # self.privmsg(vars['_source'], self.nick, None, data) + s = self.minimizeUrl(vars['_source']) + self.sendMessage(s, 'PRIVMSG', t, data) + def msg_message_echo_private(self, vars, mc, data): + pass # echo is not common in irc + def msg_message_public(self, vars, mc, data): + if vars['_source'] != self.url: # skip echo + if vars.has_key('_context'): + t = '#' + self.minimizeUrl(vars['_context']) + else: + t = '#' + self.minimizeUrl(vars['_source']) + s = self.minimizeUrl(vars['_source']) + # TODO: might be appropriate to use self.privmsg() + # self.privmsg(vars['_source'], None, t, data) + self.sendMessage(s, 'PRIVMSG', t, data) + else: + pass # echo is not common in IRC + def msg_echo_place_enter(self, vars, mc, data): + t = '#' + self.minimizeUrl(vars['_source']) + self.sendMessage(self.nick, 'JOIN', t) + def msg_echo_place_leave(self, vars, mc, data): + t = '#' + self.minimizeUrl(vars['_source']) + self.sendMessage(self.nick, 'PART', t) + def msg_status_place_members(self, vars, mc, data): + t = '#' + self.minimizeUrl(vars['_source']) + self.names(self.nick, t, map(self.minimizeUrl, vars['_list_members'])) + def msg_notice_unlink(self, vars, mc, data): + if vars['_source'] == self.url: + self.url = None + self.transport.loseConnection() + def msg_notice_place_enter(self, vars, mc, data): + s = vars['_source'] + c = '#' + self.minimizeUrl(vars['_context']) + if s == self.url: + return # we dont like being joined via notice! + self.join(self.url2prefix(self.minimizeUrl(s)), c) + def msg_notice_place_leave(self, vars, mc, data): + s = vars['_source'] + c = '#' + self.minimizeUrl(vars['_context']) + self.part(self.url2prefix(self.minimizeUrl(s)), c) + def msg_notice_roster(self, vars, mc, data): + friends = vars['_list_friends'] + places = vars['_list_places'] + if friends: + self.sNotice('Friends') + for friend in vars['_list_friends']: + self.sNotice('~ %s'%friend) + if places: + self.sNotice('Places') + for place in places: + self.sNotice('@ %s'%place) + def msg_request_friendship(self, vars, mc, data): + sni = self.minimizeUrl(vars['_source']) + self.notice(sni, self.nick, + '%s wants to be your friend'%(sni)) + def msg_notice_friendship_established(self, vars, mc, data): + sni = self.minimizeUrl(vars['_source']) + self.notice(sni, self.nick, + '%s is now your friend'%(sni)) + +class IRCDFactory(ServerFactory): + center = None + def __init__(self, center, location): + self.center = center + self.location = location + def buildProtocol(self, addr): + p = IRCD(self.center, self.location) + p.factory = self + return p + +class MyServerCenter(ServerCenter): + def create_user(self, netname): + return Person(netname, self) + def create_place(self, netname): + return Place(netname, self) + def create_context(self, netname): + return GroupSlave(netname, self) + + +root = 'psyc://ente' # TODO: this does belong into a config file! +application = service.Application('psycserver') + +center = MyServerCenter(root) + +factory = PSYCServerFactory(center, None, root) +psycServer = internet.TCPServer(4404, factory) + +ircfactory = IRCDFactory(center, root) +ircServer = internet.TCPServer(6667, ircfactory) + +myService = service.IServiceCollection(application) +psycServer.setServiceParent(myService) +ircServer.setServiceParent(myService) diff --git a/fippos-twisted/contrib/rss/README b/fippos-twisted/contrib/rss/README new file mode 100644 index 0000000..a3f0093 --- /dev/null +++ b/fippos-twisted/contrib/rss/README @@ -0,0 +1,9 @@ +Purpose: +this is designed to be a news distributing server only. It fetches RSS feeds + +Running: + twistd -noy rss_server.py +This code depends on +- rss.py from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/277099 +- feedparser.py from http://diveintomark.org/projects/feed_parser/ +- twisted python (python is less fun without) diff --git a/fippos-twisted/contrib/rss/rss_server.py b/fippos-twisted/contrib/rss/rss_server.py new file mode 100644 index 0000000..116d69e --- /dev/null +++ b/fippos-twisted/contrib/rss/rss_server.py @@ -0,0 +1,131 @@ +# usage: twistd -noy rss_server.py +from twisted.application import service, internet +from twisted.internet import reactor +from pypsyc.center import ServerCenter +from pypsyc.net import PSYCServerFactory +from pypsyc.objects.server import Place + +from pypsyc import parseUNL + +try: + from rss import FeederFactory +except ImportError: + print 'error while importing rss.py' + print 'make sure you have rss.py from ', + print 'from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/277099' + print 'and feedparser.py from ', + print 'http://diveintomark.org/projects/feed_parser/' + + +class PlaceFeeder(FeederFactory): + def getFeeds(self): + return [] + + +class Feedplace(Place): + channel = None + silent = True + def __init__(self, netname, center, feed): + Place.__init__(self, netname, center) + self.feed = feed + self.error = 0 # number of times that we didnt succeed + self.fetched = 0 # number of times we fetched sucessfully + self.fetched_items = 0 # the average number of new items per fetch + + self.fetch_interval = 15 * 60 # initial feed interval + + self.feeder = PlaceFeeder(False) + self.news = [] + reactor.callLater(5, self.fetchFeed) + def fetchFeed(self): + d = self.feeder.start([(self.feed, '')]) + d.addCallback(self.gotFeed) + d.addErrback(self.gotError) + def gotError(self, error): + self.error += 1 + # TODO: react on feeds that are temp/perm unreachable + reactor.callLater(self.fetch_interval, self.fetchFeed) + print 'looks as if feed %s is unreachable'%self.feed + print error + def gotFeed(self, data): + self.fetched += 1 + new = [] + items = {} + if self.channel is None: + self.channel = data['channel'] + self.castmsg({ '_nick' : self.netname, + '_topic' : self.showTopic()}, + '_status_place_topic', + 'Topic by [_nick]: [_topic]') + for item in data['items']: + # diff by url + href = item['link'] + new.append(href) + items[href] = item + + diff = filter(lambda x: x not in self.news, new) + for href in diff: + item = items[href] + v = {'_news_headline' : item['title_detail']['value'], + '_page_news' : href, + '_channel_title' : data['channel']['title'] } + self.castmsg(v, '_notice_news_headline_rss', + '([_channel_title]) [_news_headline]\n[_page_news]') + + self.news = new + + # feeds whose average number of new items is < x + # can be polled with less frequency + self.fetched_items += len(diff) + avg = float(self.fetched_items) / self.fetched + print 'avg no of new items per fetch for %s is %f'%(self.feed, avg) + if avg < 1.5: # x + # lower frequency + self.fetch_interval *= avg + elif avg > 4.5 and self.fetched > 10: # y + # increase frequenzy + self.fetch_interval /= 2 + print 'callLater in %d'%(self.fetch_interval) + reactor.callLater(self.fetch_interval, self.fetchFeed) + def showMembers(self): + return [] + def showTopic(self): + if self.channel is not None: + return 'feed \'%s\' available from %s'%(self.channel['title'], + self.feed) + else: + return 'stand by while fetching feed %s'%self.feed + def msg_message_public(self, vars, mc, data): + pass # they're not for talking + + +class MyServerCenter(ServerCenter): + feeds = {} + def create_user(self, netname): + return False + def create_place(self, netname): + u = parseUNL(netname) + res = u['resource'][1:] + if self.feeds.has_key(res): + return Feedplace(netname, center, self.feeds[res]) + return False + def create_context(self, netname): + return False + def setFeeds(self, feeds): + self.feeds = feeds + def getFeeds(self): + return self.feeds + + +root = 'psyc://ente' +application = service.Application('psyc news distributor') + +center = MyServerCenter(root) +factory = PSYCServerFactory(center, None, root) +psycServer = internet.TCPServer(4404, factory) + +center.setFeeds({ 'heise' : 'http://www.heise.de/newsticker/heise.rdf' }) + +myService = service.IServiceCollection(application) +psycServer.setServiceParent(myService) + diff --git a/fippos-twisted/contrib/whitepaper.py b/fippos-twisted/contrib/whitepaper.py new file mode 100644 index 0000000..1b6b17a --- /dev/null +++ b/fippos-twisted/contrib/whitepaper.py @@ -0,0 +1,28 @@ +"""p2p manager and client as described in the old PSYC whitepaper at +http://psyc.pages.de/whitepaper/ +probably broken currently""" +from pypsyc.center import ServerCenter, ClientCenter +from pypsyc.objects.PSYCObject import PSYCClient +from pypsyc.objects.Advanced import AdvancedManager, AdvancedPlace +import sys +import asyncore + +location = 'psyc://adamantine.aquarium' + +type = sys.argv[1] +if type == 'manager': + center = ServerCenter([location + ':4405/', location + ':4406', + location + ':4407', location + ':4408']) + center2 = ServerCenter([location]) + AdvancedManager(location + '/@advanced', center2) +if type == 'client': + center = ClientCenter() + me = PSYCClient(location + '/~fippo', center) + me.online() + AdvancedPlace(location + "/@advanced", center) + me.sendmsg({'_target' : location + '/@advanced', + '_source' : location + '/~fippo'}, + '_request_enter', '') + +while center: + asyncore.poll(timeout=0.5) diff --git a/fippos-twisted/doc/DESIGN b/fippos-twisted/doc/DESIGN new file mode 100644 index 0000000..56fd265 --- /dev/null +++ b/fippos-twisted/doc/DESIGN @@ -0,0 +1,10 @@ +Design of the pyPSYC library + +Asynchronus programming + +The role of the center object + +Writing a client +- GUI event loop Integration + +Writing a server diff --git a/fippos-twisted/doc/FEATURES b/fippos-twisted/doc/FEATURES new file mode 100644 index 0000000..32bf06e --- /dev/null +++ b/fippos-twisted/doc/FEATURES @@ -0,0 +1 @@ +onesided zlib compression diff --git a/fippos-twisted/pypsyc/PSYC.py b/fippos-twisted/pypsyc/PSYC.py new file mode 100644 index 0000000..8c0e04c --- /dev/null +++ b/fippos-twisted/pypsyc/PSYC.py @@ -0,0 +1,70 @@ +from pypsyc.State import State + +from twisted.protocols.basic import LineReceiver + + +class PSYCProtocol(LineReceiver): + statemachine = None + state = 'vars' + mc = None + text = None + delimiter = '\n' + initialized = False + def connectionMade(self): + self.statemachine = State() + self.reset() + self.transport.write('.\n') + def msg(self, vars, mc, text): + """serialize a packet and send to the other side + + @type vars: C{dict} + @param vars: Dictionary of variables to be serialized. + Variables should be strings or lists, variable names start + with an underscore + + @type mc: C{str} + @param mc: Methodname of the packet, starts with an underscore + + @type text: C{str} + @param text: Data part of the packet""" + packet = self.statemachine.serialize(vars) # this has a newline already! + packet += mc + '\n' + packet += text + '\n' + packet += '.\n' + self.transport.write(packet.encode('iso-8859-1')) + def reset(self): + self.statemachine.reset() + self.state = 'vars' + self.mc = '' + self.text = '' + def lineReceived(self, line): + """this does not yet handle binary mode and fragments""" + line = line.strip() + if self.initialized is False: + if line != '.': + self.msg({}, '_error_syntax_protocol_initialization', + 'The protocol begins with a dot on a line of by itself') + self.transport.loseConnection() + return + else: + self.initialized = True + return + if line == '.': + self.packetReceived() + elif self.state is 'vars' and line.startswith('_'): + self.statemachine.eat(None) + self.mc = line + self.state = 'text' + elif self.state == 'vars': + self.statemachine.eat(line) + else: + self.text += line + '\n' + def packetReceived(self): + vars = self.statemachine.copy() + peer = self.transport.getPeer() + if not vars.has_key('_source'): + vars['_source'] = 'psyc://%s:-%d'%(peer.host, peer.port) + mc = self.mc[:] + data = self.text[:].strip().decode('iso-8859-1') + self.reset() + self.factory.packetReceived(vars, mc, data, self, peer) diff --git a/fippos-twisted/pypsyc/State.py b/fippos-twisted/pypsyc/State.py new file mode 100644 index 0000000..c687165 --- /dev/null +++ b/fippos-twisted/pypsyc/State.py @@ -0,0 +1,156 @@ +# TODO: write tests +from pypsyc import MMPVARS + +from copy import deepcopy + +class SenderState: + def __init__(self): + self.laststate = {} + self.persistent_out = {} + def serializeList(self, varname, value): + t = [] + old = self.persistent_out.get(varname) + # that is far to experimental + if True: # not old: # no previous list sent + self.persistent_out[varname] = value + t.append(':%s\t%s\n'%(varname, value[0].replace('\n', '\n\t'))) + for item in value[1:]: + t.append(':\t%s\n'%(item.replace('\n', '\n\t'))) + + return '\n'.join(t) + if False: + augmented = filter(lambda x: x not in old, value) + if augmented: + packet += '+%s\t%s\n'%(varname, augmented[0].replace('\n', + '\n\t')) + for item in augmented[1:]: + packet += '+\t%s\n'%(item.replace('\n', '\n\t')) + diminished = filter(lambda x: x not in value, old) + if diminished: + packet += '-%s\t%s\n'%(varname, diminished[0].replace('\n', + '\n\t')) + for item in diminished[1:]: + packet += '-\t%s\n'%(item.replace('\n', '\n\t')) + self.persistent_out[varname] = value + def serialize(self, state): + """ + serializes a set of variables into a string + + @type state: C{dict} + @param state: Dictionary of variables to be serialized + """ + L = [] + # beware of the lambdas! + L.append(self.varencode(filter(lambda x: x[0] in MMPVARS and x[1], + state.items()))) + if L != []: + L.append('\n') + L.append(self.varencode(filter(lambda x: x[0] not in MMPVARS and x[1], + state.items()))) + self.laststate = state + bytes = '\n'.join(L) + return bytes + def varencode(self, v): + """ + encodes a set of variables, setting the state persistent according to + some strategy + + @type v: C{dict} + @param v: Dictionary of variables to be serialized + """ + t = [] + for (varname, value) in v: + if self.persistent_out.get(varname) == value: + pass + elif varname.startswith('_list') or type(value) == type([]): + t.append(self.serializeList(varname, value)) + elif self.laststate.get(varname) == value and varname != '_context': + self.persistent_out[varname] = value + t.append('=%s\t%s\n'%(varname, value.replace('\n', '\n\t'))) + else: + t.append(':%s\t%s\n'%(varname, value.replace('\n', '\n\t'))) + return '\n'.join(t) + + +class ReceiverState: + glyph = '' + varname = '' + listFlag = False + value = '' + def __init__(self): + self.state = {} + self.persistent = {} + def reset(self): + self.state = {} + self.glyph = '' + self.varname = '' + self.listFlag= False + self.value = '' + def copy(self): + # do we actually need those deep copys? TODO + t = deepcopy(self.persistent) + t.update(deepcopy(self.state)) + return t + def eat(self, line): + """ + this one is tricky... first it handles the previous line, + and then it prepares the current line. + This is needed to implement multiline-continuations in lists + + @type line: C{str} or C{None} + @param line: line to be parsed, a None signals that variable + parsing for current packet is finished + """ + if line: line = line.decode('iso-8859-1') # we use unicode internally + if line and (line[0] == ' ' or line[0] == '\t'): # multiline support + self.value += '\n' + line[1:] + return + + # glyph handling + if self.glyph == ':': + if self.listFlag: + if not type(self.state[self.varname]) == list: + self.state[self.varname] = [self.state[self.varname]] + self.state[self.varname].append(self.value) + else: + if self.varname.startswith('_list'): + self.value = [self.value] + self.state[self.varname] = self.value + elif self.glyph == '=': + if self.listFlag: + if not type(self.persistent[self.varname]) == list: + self.persistent[self.varname] = [self.self.persistent[self.varname]] + self.persistent[self.varname].append(self.value) + else: + self.persistent[self.varname] = self.value + elif self.glyph == '+': + self.persistent.get(self.varname, []).append(self.value) + elif self.glyph == '-': + raise NotImplementedError + elif self.glyph == '?': + raise NotImplementedError + + if not line: # feeding done + return + + # here we parse the current line + self.glyph = line[0] + if line[1] == '\t': + # lastvarname-optimization: varname remains the same + self.listFlag = True + self.value = line[2:] + else: + self.listFlag = False + if line.find('\t') == -1: + self.varname = line[1:] + self.value = '' + else: + self.varname, self.value = line[1:].split('\t', 1) + self.value = self.value + + +class State(SenderState, ReceiverState): + """combination of sender and receiver state""" + def __init__(self): + SenderState.__init__(self) + ReceiverState.__init__(self) diff --git a/fippos-twisted/pypsyc/__init__.py b/fippos-twisted/pypsyc/__init__.py new file mode 100644 index 0000000..bfe806d --- /dev/null +++ b/fippos-twisted/pypsyc/__init__.py @@ -0,0 +1,101 @@ +"""common methods and constants for pyPSYC""" + +# URL parsing functions modelled after psycMUVE parseURL +def parseURL(url): + u = { 'scheme' : '', + 'user' : '', + 'pass' : '', + 'host' : '', + 'port' : '4404', + 'transport' : '', + 'string' : url, + 'body' : '', + 'userAtHost' : '', + 'hostPort' : '', + 'root' : '', + 'circuit' : '', + 'size' : '' + } + if url.find(':') == -1: return u + u['scheme'], t = url.split(':', 1) + if t[0:2] == '//': t = t[2:] + u['body'] = t[:] + if t.find('/') != -1: + t, u['resource'] = t.split('/', 1) + else: + u['resource'] = '' + if u.has_key('resource') and u['resource'].find('#') != -1: + u['resource'], u['fragment'] = u['resource'].split('#', 1) + u['userAtHost'] = t[:] + if t.find('@') != -1: + s, t = t.split('@', 1) + if s.find(':') != -1: + u['user'], u['pass'] = s.split(':', 1) + else: + u['user'] = s + u['hostPort'] = t[:] + u['root'] = u['scheme'] + '://' + u['hostPort'] + if t.find(':') != -1: + t, s = t.split(':', 1) + # TODO: split s in Port (numeric), Transport + if s and s[-1] in ['c', 'd', 'm']: + u['transport'] = s[-1] + u['port'] = s[:-1] or '4404' + else: + u['port'] = s or '4404' + u['host'] = t[:] +# print "parseurl(%s)"%url, u + return u + + +def parseUNL(unl): return parseURL(unl) # alias + +def UNL2Location(unl): + # if we did not have the user@host syntax this would + # reduce to a simple splitting in front of # + u = parseUNL(unl) + short = u['scheme'] + '://' + u['host'] + if u['port'] != '4404': + short += ':' + u['port'] + if u['resource']: + return short + '/' + u['resource'] + return short + + +def netLocation(unl): + u = parseURL(unl) + return u['root'] + +def parsetext(vars, mc, data, caller=None): + pstring = data + #print '---' + #print type(data) + #print '---' + try: + for (varname, value) in vars.items(): + if type(value) == list: + no_list = u'' + for x in value: + no_list += x + ', ' + pstring = pstring.replace(u'[' + varname + u']', no_list[:-2]) + else: + pstring = pstring.replace(u'[' + varname + u']', value) + except: + print 'Error in parsetext() for vars' + return pstring + +# debugging helper +def dump_packet(banner, vars, mc, data): + print banner + ' ', + for key in vars.keys(): + try: + print key + '=' + vars[key] + ' ', + except: + pass + print mc, + print '[' + parsetext(vars, mc, data) + ']' + + +# constants +GLYPHS = [':', '=', '+', '-', '?', ' ', '\t' ] +MMPVARS = ["_source", "_target", "_context"] diff --git a/fippos-twisted/pypsyc/center.py b/fippos-twisted/pypsyc/center.py new file mode 100644 index 0000000..43b40f0 --- /dev/null +++ b/fippos-twisted/pypsyc/center.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +from pypsyc import parseUNL, UNL2Location, netLocation + +from twisted.internet import reactor, defer +from pypsyc.net import PSYCClientConnector, PSYCActiveConnector + +from pypsyc import dump_packet + + +class Center: + object_handlers = {} + remote_connections = {} + def msg(self, vars, mc, data): + raise NotImplementedError + + def sendmsg(self, vars, mc, data, target = None): + raise NotImplementedError + + def register_object(self, netname, object): + # multiple handlers are not possible + # we only want lower case netnames + self.object_handlers[netname.lower()] = object + + def find_object(self, netname): + """find a local object that corresponds to netname""" + # try "real/local" object" + # we only have lower case netnames + return self.object_handlers.get(netname.lower()) + + def find_remote(self, netname): + """find a remote location where netname may be located""" + address = netLocation(netname) + if self.remote_connections.has_key(address): + return self.remote_connections[address] + return None + def register_remote(self, obj, where): + self.remote_connections[where] = obj + def unregister_remote(self, obj, where): + if self.remote_connections.get(where) == obj: + self.remote_connections.pop(where) + def connect(self, location): + raise NotImplementedError + def create_user(self, netname): + raise NotImplementedError + def create_place(self, netname): + raise NotImplementedError + + +class ClientCenter(Center): + # connection to the homeserver, default return for find_remote + default_connection = None + options = {} + def get_option(self, option): + return self.options.get(option) + def connect(self, location): + # already connected or connecting? + a = self.find_remote(location) + if a: + return a + a = PSYCClientConnector(self, location) + return a.get_queue() + def find_remote(self, netname): + """clients will send most things via their homeserver + p2p connections are an exception to that, but they will be + created by user objects""" + return Center.find_remote(self, netname) or Center.find_remote(self, self.default_connection) + def msg(self, vars, mc, data): + if not mc: + #print "warning: minor bug in pypsyc: msg() without mc" + return + if self.get_option('debug'): + dump_packet(">>>", vars, mc, data) + source = vars.get('_context') or vars.get('_source') + if not source: return # ignore empty packets? + obj = self.find_object(source) + if obj: + obj.msg(vars, mc, data) + return + print 'unhandled packet from %s'%source + # do something about it... pop up a window, etc + u = parseUNL(source) + if u['resource'] and u['resource'].startswith('~'): + # create a user object + obj = self.create_user(source) + obj.msg(vars, mc, data) + elif u['resource'] and u['resource'].startswith('@'): + # create a place object + obj = self.create_place(source) + obj.msg(vars, mc, data) + else: + print 'no handler for %s object'%(source) + def sendmsg(self, vars, mc, data): + target = vars.get('_target') + if not target: return + obj = self.find_remote(target) + if obj: + obj.msg(vars, mc, data) + else: + raise + # this should not happen! + + +# TODO this does not belong here +import sha +class Authenticator: + def __init__(self, center, uni, password): + self.center = center + self.uni = uni + self.password = password + def startLink(self): + print 'start link' + self.sendmsg({'_target' : uni }, '_request_link', '') + def msg_query_password(self, vars, mc, data): + if vars['_nonce'] and self.center.get_option('auth_sha'): + digest = sha.sha(vars['_nonce'] + self.password).hexdigest() + self.center.sendmsg({ '_method' : 'sha1', + '_password' : digest }, + '_set_password', '') + elif self.center.get_option('auth_plain'): + self.sendmsg({ '_password' : self.password }, + '_set_password', '') + else: + print 'no authorization method available!' + return + + +class Client(ClientCenter): + default_uni = '' + nick = '' + online_callback = None + def __init__(self, config): + self.config = config + + self.default_uni = config.get('main', 'uni') + + u = parseUNL(self.default_uni) + self.nick = u['resource'][1:] + self.default_connection = netLocation(self.default_uni) + if self.config.has_section('library'): + for option in self.config.options('library'): + self.options[option] = self.config.getboolean('library', option) + def online(self): + self.connect(self.default_connection) + # experimental API using the cool Deferred's + self.online_callback = defer.Deferred() + return self.online_callback + def gotOnline(self): + if self.online_callback: + self.online_callback.callback(None) + print 'connected to host of default location' + def sendmsg(self, vars, mc, data): + if vars.get('_nick') == '': + vars['_nick'] = self.nick + # TODO: I am not sure, it this is correct + if vars.get('_source') == '': + vars['_source'] = self.default_uni + ClientCenter.sendmsg(self, vars, mc, data) + + +class ServerCenter(Center): + unl2uni = {} + remote_contexts = {} + def __init__(self, location): + self.location = location + def msg(self, vars, mc, data): + if not mc: + return # ignore empty packets + source = vars['_source'] + # TODO: auth check 'global' or per-object + if vars.has_key('_identification'): + print 'Identification of %s is %s'%(source, vars['_identification']) + context = vars.get('_context') + if context and not self.is_local_object(context): + target = context + else: + target = vars['_target'] + if self.unl2uni.has_key(source): + """local client who is using us as a proxy""" + self.unl2uni[source].sendmsg(vars, mc, data) + elif target: + obj = self.find_object(target) + if obj: + return obj.msg(vars, mc, data) + # probably object has to be created + if self.is_local_object(target): + u = parseUNL(target) + if (u['resource'] and u['resource'].startswith('@')): + obj = self.create_place(target) + if obj: + return obj.msg(vars, mc, data) + else: + vars['_target'], vars['_source'] = vars['_source'], vars['_target'] + self.sendmsg(vars, '_error_unknown_name_place', + 'No such place: [_source]') + elif (u['resource'] and u['resource'].startswith('~')): + obj = self.create_user(target) + if obj: + return obj.msg(vars, mc, data) + else: + vars['_target'], vars['_source'] = vars['_source'], vars['_target'] + self.sendmsg(vars, '_error_unknown_name_user', + 'No such user: [_source]') + elif u['resource']: + vars['_target'] = source + vars['_source'] = target + self.sendmsg(vars, '_error_unknown_name', + 'No such object: [_source]') + else: # rootmsg + pass + elif context is not None and self.remote_contexts.has_key(context): + self.remote_contexts[context].castmsg(vars, mc, data) + else: # nonlocal target + print 'rejected relay %s from %s to %s'%(mc, source, target) + return # skip it for now + self.sendmsg({ '_target' : source, '_source' : self.location, + '_destination' : target }, + '_error_rejected_relay', + 'You are not allowed to send messages to [_destination]') + else: + # no target??? + print 'no target in packet???' + + def sendmsg(self, vars, mc, data): + target = vars['_target'] + if self.is_local_object(target): + self.msg(vars, mc, data) + else: + u = parseUNL(target) + target = (self.find_object(target) or + self.find_remote(target) or + self.find_remote(target)) + if not target and u['scheme'] == 'psyc': + q = self.connect(u['root']) + q.msg(vars, mc, data) + elif target: + target.msg(vars, mc, data) + else: + raise 'can not find %s'%(target) # programming error? + def register_object(self, netname, object): + self.object_handlers[netname.lower()] = object + def link_unl(self, unl, uni): + self.unl2uni[unl] = uni + def unlink_unl(self, unl, uni): + if self.unl2uni.has_key(unl) and self.unl2uni[unl] == uni: + self.unl2uni.pop(unl) + def is_local_object(self, location): + return netLocation(location) == netLocation(self.location) + def connect(self, location): + a = PSYCActiveConnector(self, location, self.location) + self.remote_connections[location] = a.get_queue() + return a.get_queue() + def find_remote(self, netname): + address = netLocation(netname) + return self.remote_connections.get(address, None) + def join_context(self, context, obj): + if self.is_local_object(context): + print 'skipping join to local context' + if not self.remote_contexts.has_key(context): + self.remote_contexts[context] = self.create_context(context) + self.remote_contexts[context].join(obj) + def leave_context(self, context, obj): + if self.is_local_object(context): + print 'skipping part of local context' + if self.remote_contexts.has_key(context): + self.remote_contexts[context].leave(obj) + # TODO possibly clean_up that context + def create_context(self, netname): + """this is how we create context slaves""" + raise NotImplementedError diff --git a/fippos-twisted/pypsyc/net.py b/fippos-twisted/pypsyc/net.py new file mode 100644 index 0000000..a42352e --- /dev/null +++ b/fippos-twisted/pypsyc/net.py @@ -0,0 +1,186 @@ +"""network code for pyPSYC""" + +from pypsyc import GLYPHS + +from pypsyc.PSYC import PSYCProtocol +from pypsyc import parseUNL, UNL2Location + +from twisted.names import client +from twisted.internet import reactor +from twisted.internet.protocol import ClientFactory, ServerFactory + + +class HostChecker: + """checks validity of _source/_context in a packet + this could well implement policies like trusted from localhost""" + def __init__(self, center, target, myname): + self.target = target + self.myname = myname + self.center = center + def check(self, vars, mc, data, protocol, peer): + source = vars.get('_context') or vars.get('_source') + if (source and not + source.startswith('psyc://%s'%(peer.host))): + u = parseUNL(source) + d = client.lookupAddress(u['host']) + d.addCallback(self.resolved, peer.host, vars, mc, data, protocol) + elif source: + # numeric address + self.handle(vars, mc, data, protocol) + else: + protocol.msg({ '_source' : self.myname }, + '_error_syntax_protocol_missing_source', + 'Your implementation is broken') + def handle(self, vars, mc, data, protocol): + method = getattr(self, 'recv%s'%mc, None) + if method is not None: + method(vars, mc, data, protocol) + else: + u = parseUNL(vars['_source']) + self.center.register_remote(protocol, u['root']) + self.center.msg(vars, mc, data) + def resolved(self, record, shouldbe, vars, mc, data, protocol): + v = { '_source' : self.myname, + '_target' : vars['_source'] } + if not record[0]: + print 'name not resolvable' + protocol.msg(v, '_error_rejected_relay', + '[_source] is not resolvable. Goodbye') + protocol.transport.loseConnection() + elif record[0][0].type == 5: + payload = record[0][0].payload + d = client.lookupAddress(payload.name.name) + d.addCallback(self.resolved, shouldbe, vars, mc, data, protocol) + else: + payload = record[0][0].payload + if payload.dottedQuad() == shouldbe: + self.handle(vars, mc, data, protocol) + else: + print 'rejected relay' + protocol.msg(v, '_error_rejected_relay', + '[_source] does not resolve to your ip.') + protocol.transport.loseConnection() + + +class PSYCConnector: + hostname = '' + port = 4404 + factory = None + factory_type = None + real_hostname = '' + def __init__(self, center, target, myname = None): + # TODO: dont try to resolve IP addresses ;) + # must subclass + if not self.factory_type: raise NotImplementedError + try: + self.factory = self.factory_type(center, target, myname) + except: + print self.factory_type + raise + u = parseUNL(target) + self.host = u['host'] + d = client.lookupService('_psyc._tcp.' + self.host) + d.addCallback(self.srvResolved) + def get_queue(self): + return self.factory + def resolved(self, record): + if not record[0]: + print 'resolution failed...' + else: + payload = record[0][0].payload + if record[0][0].type == 5: # evil cname + d = client.lookupAddress(payload.name.name) + d.addCallback(self.resolved) + else: + reactor.connectTCP(payload.dottedQuad(), self.port, self.factory) + return True + def srvResolved(self, record): + if not record[0]: + d = client.lookupAddress(self.host) + d.addCallback(self.resolved) + else: + payload = record[0][0].payload + self.port = payload.port + d = client.lookupAddress(payload.target.name) + d.addCallback(self.resolved) + return True + + +class PSYCClientFactory(ClientFactory): + """a factory for a client which does not have to check + for validity of host names""" + class PSYCClient(PSYCProtocol): + def connectionMade(self): + PSYCProtocol.connectionMade(self) + self.factory.connectionMade(self) + + protocol = PSYCClient + center = None + location = None + def __init__(self, center, location, myname): + self.center = center + self.location = location + def connectionMade(self, proto): + self.center.register_remote(proto, self.location) + self.center.gotOnline() + def packetReceived(self, vars, mc, data, protocol, peer): + self.center.msg(vars, mc, data) + + +class PSYCClientConnector(PSYCConnector): + factory_type = PSYCClientFactory + + +class PSYCActiveFactory(ClientFactory, HostChecker): + """a factory for a client which does hostname checks + maybe this will also become a Q + probably we want to override connectionMade?""" + protocol = PSYCProtocol + queue = [] + connected = None + def packetReceived(self, vars, mc, data, protocol, peer): + self.check(vars, mc, data, protocol, peer) + def msg(self, vars, mc, data): + # watch out, if we dont use dict-vars we may need deepcopy + if self.connected is not None: + self.connected.msg(vars, mc, data) + else: + self.queue.append((vars, mc, data)) + def run_queue(self, target): + for (vars, mc, data) in self.queue: + target.msg(vars, mc, data) + self.queue = [] + self.connected = target + def recv_notice_circuit_established(self, vars, mc, data, protocol): + print 'got _notice_circuit_established' + protocol.msg({'_source' : self.myname, + '_target' : self.target or vars['_source'] }, + '_notice_circuit_established', + 'hi there') + # TODO: what do we register here? source oder our definition of + # what the source should be (self.target) + self.center.register_remote(protocol, self.target) + self.run_queue(protocol) + + +class PSYCActiveConnector(PSYCConnector): + factory_type = PSYCActiveFactory + + +class PSYCServerFactory(ServerFactory, HostChecker): + """funny question... do we deal all the stuff about + modules, compression, tls etc here?""" + class PSYCServerProtocol(PSYCProtocol): + def connectionMade(self): + peer = self.transport.getPeer() + PSYCProtocol.connectionMade(self) + self.msg({ '_source' : self.factory.myname, + '_target' : 'psyc://%s:-%d'%(peer.host, peer.port)}, + '_notice_circuit_established', + 'Connection to [_source] established') + + protocol = PSYCServerProtocol + def packetReceived(self, vars, mc, data, protocol, peer): + self.check(vars, mc, data, protocol, peer) + def recv_notice_circuit_established(self, vars, mc, data, protocol): + self.center.register_remote(protocol, vars['_source']) diff --git a/fippos-twisted/pypsyc/objects/__init__.py b/fippos-twisted/pypsyc/objects/__init__.py new file mode 100644 index 0000000..42973b6 --- /dev/null +++ b/fippos-twisted/pypsyc/objects/__init__.py @@ -0,0 +1,49 @@ +class PSYCReceiver: + def msg(self, vars, mc, data): + 'called when a message is received' + # method inheritance + if mc.count('_') > 10: + # considered abusive + return + + l = len(mc) + while l > 0: + method = getattr(self, 'msg%s'%(mc[:l]), None) + if method is not None: + # could this method return the next methodname + # to be called? freaky! + # actually, this a much more flexible fallthrough + # than switch/case provides. + # yet it is more expensive to evaluate + method(vars, mc, data) + break + l = mc.rfind('_', 0, l) + else: + self.msgUnknownMethod(vars, mc, data) + def msgUnknownMethod(self, vars, mc, data): + print 'unknown %s'%mc + + +class PSYCObject(PSYCReceiver): + """generic PSYC object""" + def __init__(self, netname, center): + self.center = center + self.netname = netname.lower() + self.center.register_object(netname, self) + def url(self): + return self.netname + def str(self): + return self.netname + def sendmsg(self, vars, mc, data): + 'called to send a message' + l = len(mc) + while l > 0: + method = getattr(self, 'sendmsg%s'%(mc[:l]), None) + if method is not None: + method(vars, mc, data) + break + l = mc.rfind('_', 0, l) + else: + self.center.sendmsg(vars, mc, data) + def castmsg(self, vars, mc, data): + 'called to send a message to a group' diff --git a/fippos-twisted/pypsyc/objects/client.py b/fippos-twisted/pypsyc/objects/client.py new file mode 100644 index 0000000..0177a3d --- /dev/null +++ b/fippos-twisted/pypsyc/objects/client.py @@ -0,0 +1,14 @@ +"""Client API for PSYC objects""" + +class IPSYCClientObject: + def msg(self, vars, mc, data): + """receive a message""" + def sendmsg(self, vars, mc, data): + """send a message to a single person or a group manager + use this make requests to the other side that should not be + distributed in an unchangend fashion""" + def castmsg(self, vars, mc, data): + """send a message that is destined to be delivered to a group + Note that you should use this for all communication with the + group, as it enables transparent distribution for both centralistic + and peer2peer scenarios""" diff --git a/fippos-twisted/pypsyc/objects/server.py b/fippos-twisted/pypsyc/objects/server.py new file mode 100644 index 0000000..f10a848 --- /dev/null +++ b/fippos-twisted/pypsyc/objects/server.py @@ -0,0 +1,319 @@ +from pypsyc.objects import PSYCObject +from pypsyc import parseUNL + +from twisted.internet import defer + +class GroupMaster(PSYCObject): + """ + @type members: C{dict} + @ivar members: list of members of the group + """ + def __init__(self, netname, center): + PSYCObject.__init__(self, netname, center) + self.members = {} + self.flat = {} + def sizeof(self): + return len(self.flat) + def remove(self, whom, origin = None): + self.flat.pop(whom, None) + if origin is not None: + self.members.get(origin, {}).pop(whom, None) + if not self.members.get(origin, None): + self.members.pop(origin, None) + else: + self.members.pop(whom, None) + def insert(self, whom, origin = None, data = None): + if not data: data = True + self.flat[whom] = data + if origin and not self.netname.startswith(origin): + if not self.members.get(origin): + self.members[origin] = {} + self.members[origin][whom] = True + else: + self.members[whom] = True + def getData(self, whose): + return self.flat[whose] + def setData(self, whose, data): + self.flat[whose] = data + def member(self, who): + return who in self.flat + def castmsg(self, vars, mc, data): + vars['_nick_place'] = 'muh' # IMHO _nick_place is superflous + vars['_context'] = self.netname + for (route, list) in self.members.items(): + # TODO: deepcopy needed? + vars['_target'] = route + self.center.sendmsg(vars, mc, data) + + +class GroupSlave: + def __init__(self, netname, center): + self.center = center + self.context = netname + self.members = {} + def join(self, obj): + url = obj.url() + self.members[url] = obj + # TODO: install a watcher on memberobj? + def leave(self, obj): + url = obj.url() + if url in self.members: + self.members.pop(url) + def castmsg(self, vars, mc, data): + for (target, obj) in self.members.items(): + vars['_target'] = target + obj.msg(vars, mc, data) + + +class Place(GroupMaster): + silent = False + def __init__(self, netname, center): + self.idcallbacks = {} + self.identifications = {} + self.reverseidentifications = {} + GroupMaster.__init__(self, netname, center) + def sendmsg(self, vars, mc, data): + # things like setting vars['_nick_place'] + vars['_source'] = self.netname + t = self.reverseidentifications.get(vars['_target']) + if t: + print 'setting target from %s to %s'%(vars['_target'], t) + vars['_target'] = t + GroupMaster.sendmsg(self, vars, mc, data) + def showMembers(self): + return self.flat.keys() + def showTopic(self): + return '' + def msg(self, vars, mc, data): + if '_context' in vars: + print '%s got %s with context %s from %s, bad' % (self.netname, mc, + vars['_context'], + vars['_source']) + return + ident = vars.get('_identification') + source = vars['_source'] + if ident and self.identifications.get(source) == ident: + print 'ident of %s is %s'%(source, + self.identifications[vars['_source']]) + vars['_source'] = ident + vars.pop('_identification') + GroupMaster.msg(self, vars, mc, data) + def msg_error_invalid_authentication(self, vars, mc, data): + print 'invalid auth' + d = self.idcallbacks.pop((vars['_location'], vars['_source']), None) + if d: + d.errback(1) + def msg_notice_authentication(self, vars, mc, data): + print 'valid auth' + d = self.idcallbacks.pop((vars['_location'], vars['_source']), None) + if d: + self.identifications[vars['_location']] = vars['_source'] + self.reverseidentifications[vars['_source']] = vars['_location'] + d.callback(1) + def helper(self, res, vars, mc, data): + self.msg(vars, mc, data) + def msg_request_enter(self, vars, mc, data): + source = vars['_source'] + if '_identification' in vars: + ident = vars.get('_identification') + print 'looking up identification %s'%(ident,) + self.sendmsg({ '_target' : ident, + '_location' : source }, + '_request_authenticate', 'Is that really you?') + d = defer.Deferred() + d.addCallback(self.helper, vars, mc, data) + d.addErrback(self.helper, vars, mc, data) + self.idcallbacks[(source, ident)] = d + return + # TODO: while it is not the final plan to make this via parseUNL + # it is acceptable for now + origin = parseUNL(source)['root'] + v = { '_target' : source, + '_source' : self.netname } + if '_tag' in vars: + v['_tag'] = vars['_tag'] + else: + pass + if '_nick' in vars: + v['_nick'] = vars['_nick'] + else: + pass + if self.silent is True: + v['_control'] = '_silent' + self.sendmsg(v, '_echo_place_enter', + '[_nick] enters') + v.pop('_control', None) + v.pop('_tag', None) + + v['_list_members'] = self.showMembers() + [ source ] + self.sendmsg(v, '_status_place_members', '...') + + v.pop('_list_members') + if not self.member(source): + v['_source'] = source + self.castmsg(v, '_notice_place_enter', '[_nick] enters') + self.insert(vars['_source'], origin) + def msg_request_leave(self, vars, mc, data): + source = vars['_source'] + # TODO again + origin = parseUNL(source)['root'] + v = { '_source' : source } + if self.member(source): + self.castmsg({ '_source' : source }, '_notice_place_leave', + '[_nick] leaves') + self.remove(source) + else: # not a member for whatever reason + self.sendmsg({ '_target' : source}, '_echo_place_leave', + 'You are not even a member') + pass + if self.sizeof() == 0: + # empty + pass + def msg_message_public(self, vars, mc, data): + if self.silent is False and self.member(vars['_source']): + self.castmsg(vars, mc, data) + + +class Person(GroupMaster): + def __init__(self, netname, center): + GroupMaster.__init__(self, netname, center) + self.location = None + self.forward = False + self.places = {} + self.tags = {} + def disconnected(self, prot, loc): + """client has closed connection""" + self.location = None + print 'should leave', places.keys() + self.center.disconnected(prot, loc) + print self.center.remote_connections + def msg(self, vars, mc, data): + if '_context' in vars and not vars['_context'] in self.places: + # context faking, should be impossible as of now + print 'catching fake context!' + return + # dont forward messages from our location to it... + self.forward = vars['_source'] is not self.location + GroupMaster.msg(self, vars, mc, data) + if self.forward is True and self.location is not None: + vars['_target'] = self.location + self.sendmsg(vars, mc, data) + self.forward = False + # user is offline + def sendmsg(self, vars, mc, data): + # TODO: things like tag-creation for joins belong here + # as well as setting vars['_nick'] + if not '_source' in vars or vars['_source'] == self.location: + vars['_source'] = self.netname + if vars['_target'] == self.location: + self.center.sendmsg(vars, mc, data) + else: + GroupMaster.sendmsg(self, vars, mc, data) + def sendmsg_request_enter(self, vars, mc, data): + import random + t = str(random.randint(0, 10000)) + self.tags[vars['_target']] = t + vars['_tag'] = t + # tobi says, tags become invalid after a certain amount of time + self.center.sendmsg(vars, mc, data) + def sendmsg_request_leave(self, vars, mc, data): + # prepare to leave context after a reasonable amount of time + self.center.sendmsg(vars, mc, data) + def msgUnknownMethod(self, vars, mc, data): + if not self.location: + print 'person:unknown %s for offline user'%mc + def msg_request_link(self, vars, mc, data): + if vars['_password']: + self.msg_set_password(vars, mc, data) + else: + self.sendmsg({ '_target' : vars['_source'] }, + '_query_password', 'Is that really you?') + + self.forward = False + def msg_set_password(self, vars, mc, data): + """note that we have NO authorization""" + # TODO: here we have to use origin + if self.location is not None: + self.sendmsg({ '_target' : self.location}, + '_notice_unlink', + 'You have just lost the magic feeling.') + self.location = vars['_source'] + # TODO: we may need to deregister this thing... + self.center.link_unl(self.location, self) + self.sendmsg({ '_target' : self.location}, + '_notice_link', 'You are now connected') + self.forward = False + def msg_request_unlink(self, vars, mc, data): + if vars['_source'] == self.netname: # heh, this one is nifty! + self.sendmsg({'_source' : self.netname, + '_target' : self.location }, + '_notice_unlink', + 'You have just lost the magic feeling.') + self.location = None + self.center.unlink_unl(self.location, self.netname) + for place in self.places.copy(): + self.sendmsg({'_source' : self.netname, + '_target' : place }, + '_request_leave_logout', + 'l8er') + self.forward = False + def msg_echo_place_enter(self, vars, mc, data): + source = vars['_source'] + tag = vars.get('_tag') + if not source in self.tags: + self.forward = False + elif self.tags[source] != tag: + self.tags.pop(source) # wrong tag, you get invalid + self.forward = False + else: + self.places[vars['_source']] = 1 + self.center.join_context(source, self) + def msg_notice_place_leave(self, vars, mc, data): + if vars['_source'] == self.netname: + mc = '_echo' + mc[7:] + vars['_source'] = vars['_context'] + self.msg(vars, mc, data) + def msg_echo_place_leave(self, vars, mc, data): + place = vars.get('_context') or vars['_source'] + if place in self.places: + self.places.pop(place) + self.forward = True + def msg_request_roster(self, vars, mc, data): + print 'Roster: %s'%(self.flat) + self.sendmsg({'_source' : self.netname, + '_target' : self.location, + '_list_places' : self.places, + '_list_friends' : self.flat.keys()}, + '_notice_roster', + 'Your friends are [_friends], you have entered in [_list_places].') + self.forward = False + def msg_request_friendship(self, vars, mc, data): + """ + Friendship states: + known -> 0 + asked from -> 1 + offered to -> 2 + dual accepted -> 3 + """ + source = vars['_source'] + if self.member(source) and self.getData(source) == 2: + self.setData(source, 3) + mc = '_notice_friendship_establshed' + # TODO + else: + self.insert(source, None, 1) + self.forward = True + def sendmsg_request_friendship(self, vars, mc, data): + target = vars['_target'] + if self.member(target) and self.getData(target) == 1: + self.setData(target, 3) + mc = '_notice_friendship_established' + # TODO: echo_notice_friendship_established + else: + self.insert(target, None, 2) + self.center.sendmsg(vars, mc, data) + self.forward = True + def msg_notice_friendship_established(self, vars, mc, data): + source = vars['_source'] + if self.member(source) and self.getData(source) == 2: + self.setData(source, 3) diff --git a/fippos-twisted/test.py b/fippos-twisted/test.py new file mode 100644 index 0000000..1358429 --- /dev/null +++ b/fippos-twisted/test.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +from pypsyc import parseUNL, UNL2Location, netLocation + +from pypsyc.objects.PSYCObject import PSYCQueueObject, PSYCObject, PSYCUNI, PSYCPlace, PSYCClient, ClientUser, ClientPlace, AdvancedManager, AdvancedPlace + +from pypsyc.net import PSYCUDPSender + +from pypsyc.center import ServerCenter, ClientCenter + +import asyncore + +if __name__ == '__main__': + import sys + type = sys.argv[1] + location = 'psyc://adamantine.aquarium' + center = None + if type == "server": + # tcp only server + center = ServerCenter([location + ':c']) + PSYCPlace(location + '/@place', center) + PSYCUNI(location + '/~fippo', center) + + if type == "server2": + # tcp and udp server on non-standard port + center = ServerCenter([location + ':4405']) + center.connect(location) + if type == "udpserver": + center = ServerCenter([location + ':4405d']) + PSYCUNI(location + ':4405/~fool', center) + if type == "udpclient": + # this should better be done via a Center which can parse + # URLs and them handle according to their transport + # but for a quick udp sender this is okay... + q = PSYCUDPSender(location + ':4405d') + q.msg({'_target' : 'psyc://adamantine.aquarium:d/~fippo'}, + {'_nick' : 'udpclient'}, + '_message_private', + 'hallo udp welt') + if type == "client": + center = ClientCenter() + PSYCObject('psyc://adamantine.aquarium', center) + # maybe add config information here? + # and let this thing connect as well? + me = PSYCClient('psyc://adamantine.aquarium/~fippo', center) + me.online() + # but thats the fast way to do it + me.sendmsg({'_target' : 'psyc://adamantine.aquarium/~fippo'}, + {'_password' : 'xfippox'}, + '_request_link', + '') + + + while center: + asyncore.poll(timeout=0.5) diff --git a/gtkpypsyc/client.py b/gtkpypsyc/client.py new file mode 100644 index 0000000..2662ecc --- /dev/null +++ b/gtkpypsyc/client.py @@ -0,0 +1,66 @@ +import gtk +import ConfigParser +import os, sys +import encodings.iso8859_1, encodings.ascii, encodings.cp1250, encodings.utf_8, encodings.utf_16 + +from pypsyc.center import Client + +from tabs.place import PlaceTab +from tabs.user import UserTab +from tabs.rss import RSSTab + + +class GTKClient(Client): + def __init__(self, config): + Client.__init__(self, config) + self.places_window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.places_window.connect('delete_event', + lambda w, e: gtk.main_quit()) + self.notebook = gtk.Notebook() + self.notebook.set_tab_pos(gtk.POS_BOTTOM) + self.places_window.add(self.notebook) + #self.places_window.show_all() + def show(self): + self.places_window.show_all() + return gtk.FALSE + def create_place(self, netname): + place = PlaceTab(netname, self) + self.notebook.append_page(place, gtk.Label(netname)) + self.notebook.show_all() + return place + +def poll(): + import asyncore + asyncore.poll(timeout = 0.0) + gtk.timeout_add(250, poll) # 0.25 secs + + +if __name__ == '__main__': + import sys + config = ConfigParser.ConfigParser() + if sys.platform == 'win32': + os.environ['PATH'] += ';lib;' + config.read('.pypsycrc') + else: + config.read(os.getenv('HOME') + '/.pypsycrc') + center = GTKClient(config) + + splash = gtk.Window() + splash.set_decorated(gtk.FALSE) + img = gtk.Image() + img.set_from_file(config.get('interface', + 'splashscreen')) + splash.add(img) + img.show() + splash.show() + gtk.timeout_add(3500, splash.hide) + gtk.timeout_add(4000, center.show) + + rsstab = RSSTab('psyc://adamantine.aquarium/@heise', center) + center.notebook.append_page(rsstab, gtk.Label('heise rss')) + usertab = UserTab('psyc://adamantine.aquarium/~fool', center) + center.notebook.append_page(usertab, gtk.Label('user dialog')) + + center.online() + poll() + gtk.main() diff --git a/gtkpypsyc/demo.py b/gtkpypsyc/demo.py new file mode 100644 index 0000000..64fb46e --- /dev/null +++ b/gtkpypsyc/demo.py @@ -0,0 +1,67 @@ +#!/usr/bin/envv python + +import gtk +import gobject + +class UI: + def __init__(self): + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.netobjects = {} + + self.model = gtk.TreeStore(gtk.gdk.Pixbuf, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN) + self.view = gtk.TreeView(self.model) + self.view.set_headers_visible(gtk.FALSE) + + + pxrenderer = gtk.CellRendererPixbuf() + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn() + column.pack_start(pxrenderer, gtk.FALSE) + column.pack_end(renderer, gtk.TRUE) + column.set_attributes(pxrenderer, pixbuf=0, visible=2) + column.set_attributes(renderer, text=1) + self.view.append_column(column) + + self.view.show() + + vbox = gtk.VBox() + vbox.show() + vbox.pack_start(self.view) + + combo = gtk.Combo() + # TODO: disable editing of current entry, even cursor + combo.set_popdown_strings(['Offline', 'Online']) + combo.entry.set_editable(gtk.FALSE) + combo.show() + vbox.pack_end(combo, gtk.FALSE) + + self.friendstree = self.model.insert(None, 0, [None, 'Friends', False]) + self.placetree = self.model.insert(None, 1, [None, 'Places', False]) + + self.append_person('fippo') + self.append_place('PSYC') + + gtk.timeout_add(1000, self.change_icon, 1) + gtk.timeout_add(3000, self.change_icon, 2) + gtk.timeout_add(5000, self.change_icon, 3) + + self.view.expand_all() + + self.window.add(vbox) + self.window.show() + self.window.connect('delete_event', lambda e, w: gtk.main_quit()) + def append_person(self, name): + self.model.insert(self.friendstree, -1, [None, name, True]) + def append_place(self, name): + image = gtk.Image() + image.set_from_file('./pix/place/icon_f01.png') + pxbuf = image.get_pixbuf() + self.netobjects[name] = self.model.insert(self.placetree, -1, [pxbuf, name, True]) + def change_icon(self, image): + print 'change', image + return gtk.FALSE # call only once + +u = UI() +gtk.mainloop() diff --git a/gtkpypsyc/list.py b/gtkpypsyc/list.py new file mode 100644 index 0000000..49360a0 --- /dev/null +++ b/gtkpypsyc/list.py @@ -0,0 +1,71 @@ +#!/usr/bin/envv python + +import gtk +import gobject + + +class ListWindow: + def __init__(self): + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.netobjects = {} + + self.model = gtk.TreeStore(gtk.gdk.Pixbuf, + str, # _nick_place + str, # _source/target/context + gobject.TYPE_BOOLEAN) + self.view = gtk.TreeView(self.model) + self.view.set_headers_visible(gtk.FALSE) + + pxrenderer = gtk.CellRendererPixbuf() + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn() + column.pack_start(pxrenderer, gtk.FALSE) + column.pack_end(renderer, gtk.TRUE) + column.set_attributes(pxrenderer, pixbuf=0, visible=3) + column.set_attributes(renderer, text=1) + self.view.append_column(column) + + self.view.show() + + vbox = gtk.VBox() + vbox.show() + vbox.pack_start(self.view) + + combo = gtk.Combo() + # TODO: disable editing of current entry, even cursor + # combobox may be false + combo.set_popdown_strings(['Offline', 'Online']) + combo.entry.set_editable(gtk.FALSE) + combo.show() + vbox.pack_end(combo, gtk.FALSE) + + self.friendstree = self.model.insert(None, 0, [None, 'Friends', '', False]) + self.placetree = self.model.insert(None, 1, [None, 'Places', '', False]) + + #gtk.timeout_add(1000, self.change_icon, 1) + + self.view.expand_all() + + self.window.add(vbox) + self.window.show() + #self.window.connect('delete_event', lambda e, w: gtk.main_quit()) +# def change_icon(self, image): +# return gtk.FALSE # call only once + def msg(self, vars, mc, data, caller): + if mc.startswith('_notice_place_enter'): + # only if source == self! + image = gtk.Image() + image.set_from_file('./pix/place/icon_f01.png') + self.netobjects[vars['_context']] = self.model.insert(self.placetree, -1, + [image.get_pixbuf(), + vars['_nick_place'], + vars['_context'], True]) + return + if mc.startswith('_notice_friend_present'): + image = gtk.Image() + image.set_from_file('./pix/friend/present.png') + self.netobjects[vars['_source']] = self.model.insert(self.friendstree, -1, + [image.get_pixbuf(), + vars['_nick'], + vars['_source'], True]) + return diff --git a/gtkpypsyc/rcfile b/gtkpypsyc/rcfile new file mode 100644 index 0000000..3f09dbf --- /dev/null +++ b/gtkpypsyc/rcfile @@ -0,0 +1,6 @@ +[main] +uni: psyc://your.host/~yournick +password: yourpass + +[interface] +splashscreen: pix/symlynX.gif diff --git a/gtkpypsyc/tabs/__init__.py b/gtkpypsyc/tabs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gtkpypsyc/tabs/place.py b/gtkpypsyc/tabs/place.py new file mode 100644 index 0000000..608aef7 --- /dev/null +++ b/gtkpypsyc/tabs/place.py @@ -0,0 +1,92 @@ +import gtk +from pypsyc.objects import PSYCObject + +class PlaceTab(gtk.Frame, PSYCObject): + def __init__(self, netname, center): + gtk.Frame.__init__(self, label = netname) + PSYCObject.__init__(self, netname, center) + + vbox = gtk.VBox() + self.add(vbox) + + self.textview = gtk.TextView() + self.textview.set_cursor_visible(gtk.FALSE) + self.textview.set_wrap_mode(gtk.WRAP_WORD) + self.textview.set_editable(gtk.FALSE) + self.textbuf = self.textview.get_buffer() + + hbox = gtk.HPaned() + hbox.pack1(self.textview) + vbox.pack_start(hbox) + + # uni, nick + self.nicklist = gtk.ListStore(str, str) + self.nicklist.set_sort_column_id(1, gtk.SORT_ASCENDING) + nickview = gtk.TreeView() + nickview.set_model(self.nicklist) + nickview.set_headers_visible(gtk.FALSE) + + col = gtk.TreeViewColumn('Nick', gtk.CellRendererText(), text = 1) + col.set_resizable(gtk.FALSE) + nickview.append_column(col) + + hbox.pack2(nickview, shrink=gtk.FALSE, resize=gtk.FALSE) + + self.entry = gtk.Entry() + self.entry.set_text('') + self.entry.connect('activate', self.onEntry) + vbox.pack_end(self.entry, gtk.FALSE) + self.show_all() + def append_text(self, text): + iter = self.textbuf.get_end_iter() + text = text.encode('utf-8') + '\n' + self.textbuf.insert(iter, text) + iter = self.textbuf.get_end_iter() + self.textbuf.place_cursor(iter) + mark = self.textbuf.get_insert() + self.textview.scroll_mark_onscreen(mark) + def onEntry(self, widget): + text = self.entry.get_text() + #print self.netname, '>>', text + self.entry.set_text('') + # handle a command... yes this should not be done that way + if text and text[0] == '/': + cmd = text[1:].upper() + if cmd == 'PART': + self.sendmsg({'_target' : self.netname}, + {}, + '_request_leave', + '') + return + self.sendmsg({'_target' : self.netname}, + {}, + '_message_public', + text) + def msg(self, vars, mc, data, caller): + if mc == '_message_public': + self.append_text('%s: %s'%(vars['_nick'], data)) + return + if mc == '_message_public_question': + self.append_text('%s %s: %s'%(vars['_nick'], + 'fragt', + data)) + return + if mc == '_message_public_text_action': + self.append_text('%s %s: %s'%(vars['_nick'], + vars['_action'], + data)) + return + if mc == '_status_place_members': + # _list_members, _list_members_nicks + for i in range(0, len(vars['_list_members'])): + self.nicklist.append((vars['_list_members'][i], + vars['_list_members_nicks'][i])) + return + if mc.startswith('_notice_place_leave'): + return + if mc.startswith('_notice_place_enter'): + self.nicklist.append((vars['_source'], vars['_nick'])) + return + PSYCObject.msg(self, vars, mc, data, caller) + + diff --git a/gtkpypsyc/tabs/rss.py b/gtkpypsyc/tabs/rss.py new file mode 100644 index 0000000..a1db525 --- /dev/null +++ b/gtkpypsyc/tabs/rss.py @@ -0,0 +1,65 @@ +import gtk +from pypsyc.objects import PSYCObject + +class RSSTab(gtk.Frame, PSYCObject): + def __init__(self, netname, center): + import gobject + gtk.Frame.__init__(self, label = netname) + PSYCObject.__init__(self, netname, center) + + vbox = gtk.VPaned() + self.add(vbox) + + self.label = gtk.Label() + self.model = gtk.ListStore(str, str, str) + self.tree = gtk.TreeView(self.model) + + # demos + self.model.append(['Handy-Flatrate von DoCoMo in Japan'.encode('utf-8'), + 'http://heise.de/newsticker/meldung/44883', + 'Ab Sommer 2004 will auch DoCoMo den Japanern für umgerechnet 31 Euro im Monat eine mobile Flatrate anbieten. mehr...' ]) + + + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("Headline", renderer, text=0) + self.tree.append_column(column) + + self.tree.connect('row-activated', self.preview) + + s1 = gtk.ScrolledWindow() + s1.add(self.tree) + s1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + vbox.pack1(s1, gtk.TRUE) + + self.textview = gtk.TextView() + self.textview.set_cursor_visible(gtk.FALSE) + self.textview.set_wrap_mode(gtk.WRAP_WORD) + self.textview.set_editable(gtk.FALSE) + self.textbuf = self.textview.get_buffer() + + s2 = gtk.ScrolledWindow() + s2.add(self.textview) + s2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + vbox.pack2(s2) + + #vbox.pack_end(self.textview, gtk.FALSE) + self.show_all() + def msg(self, vars, mc, data, caller): + if mc == '_status_place_description_news_rss': + self.label.set_label(data) + self.show_all() + return + if mc == '_notice_news_headline_rss': + renderer = gtk.CellRendererText() + text = vars.get('_news_headline').encode('utf-8') + column = gtk.TreeViewColumn(vars.get('_news_headline'), renderer, text=1) + self.tree.append_column(column) + self.show_all() + return + PSYCObject.msg(self, vars, mc, data, caller) + def preview(self, widget, path, column): + model, iter = self.tree.get_selection().get_selected() + self.set_text(model.get_value(iter, 1) + '\n' + model.get_value(iter, 2)) + def set_text(self, text): + text = text.encode('utf-8') + self.textbuf.set_text(text) diff --git a/gtkpypsyc/tabs/user.py b/gtkpypsyc/tabs/user.py new file mode 100644 index 0000000..0678e9d --- /dev/null +++ b/gtkpypsyc/tabs/user.py @@ -0,0 +1,30 @@ +import gtk +from pypsyc.objects import PSYCObject + +class UserTab(gtk.Frame): + def __init__(self, netname, center): + gtk.Frame.__init__(self, label = netname) + self.textview = gtk.TextView() + self.textview.set_cursor_visible(gtk.FALSE) + self.textview.set_wrap_mode(gtk.WRAP_WORD) + self.textview.set_editable(gtk.FALSE) + self.textbuf = self.textview.get_buffer() + s = gtk.ScrolledWindow() + s.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + s.add(self.textview) + + pane = gtk.VPaned() + #pane.pack1(s) + + h = gtk.VBox() + pane.pack2(h, shrink=gtk.FALSE, resize = gtk.FALSE) + pane.pack1(s) + box = gtk.HBox() + box.pack_start(gtk.Button(label='spacing'), expand=gtk.FALSE) + h.pack_start(box, expand=gtk.FALSE) + + self.entryfield = gtk.Entry() + h.pack_start(self.entryfield, expand=gtk.FALSE) + + self.add(pane) + self.show_all() diff --git a/gui-client.py b/gui-client.py new file mode 100755 index 0000000..989be5a --- /dev/null +++ b/gui-client.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# get stuff above at twistedmatrix.com +import ConfigParser +import os +import sys +# here we start importing our own modules +from pypsyc.PSYC.PSYCMessagecenter import PSYCMessagecenter +# import packages +from pypsyc.PSYC.PSYCRoom import Conferencing as ConferencingPackage +from pypsyc.PSYC.PSYCRoom import Friends as FriendsPackage +from pypsyc.PSYC.PSYCRoom import User as UserPackage +from pypsyc.PSYC.PSYCRoom import Authentication as AuthenticationPackage +from pypsyc.PSYC.PSYCRoom import Devel as DevelPackage +#import Listener + +# for linux/posix this should work +CONFIG_FILE = os.getenv("HOME") + "/.pypsyc/config" + +# windows users should uncomment the next line and comment the one above +# CONFIG_FILE = 'config' + +config = ConfigParser.ConfigParser() +config.read(CONFIG_FILE) + +center = PSYCMessagecenter(config) + +gui = None +Gui = None + +try: + guitype = config.get("gui", "type") + if guitype == "Tkinter": + import GUI.Tkinter.Gui as Gui + elif guitype == "Qt": + import GUI.Qt.Gui as Gui + elif guitype == 'wx': + import GUI.wx.devGui as Gui + + if Gui: + if guitype == 'wx': + gui = Gui.Application(sys.argv, center, config) + else: + gui = Gui.Application(sys.argv, center) + ## hier muss man besser entscheiden, was ein Toplevel() und was ein Tk() ist! + if config.get("packages", "conferencing") == "enabled": + conferencing_gui = Gui.RoomGui() + center.register(ConferencingPackage(conferencing_gui)) + + if config.get("packages", "friends") == "enabled": + friendlist = Gui.FriendList() + center.register(FriendsPackage(friendlist)) + + if config.get("packages", "user") == "enabled": + usergui = Gui.UserGui() + center.register(UserPackage(usergui)) + + if config.get("packages", "devel") == "enabled": + debuggui = Gui.MainWindow(center) + debuggui.title("debug window") + center.register(DevelPackage(debuggui)) + + ## hier was in der Art von setMainWindow() +except ConfigParser.NoSectionError: + print "Error reading config file" + +center.register(AuthenticationPackage(config)) + +gui.run() + diff --git a/in-silico/gui/__init__.py b/in-silico/gui/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/in-silico/gui/__init__.py @@ -0,0 +1 @@ + diff --git a/in-silico/gui/displays.py b/in-silico/gui/displays.py new file mode 100644 index 0000000..bb5af01 --- /dev/null +++ b/in-silico/gui/displays.py @@ -0,0 +1,248 @@ +# -*- coding: latin-1 -*- +#from wxPython.wx import * +import wx +# XXX Please feel free to modify and vershclimmbesser this piece of code +# especially the bits marked with XXX +##from pypsyc.objects.PSYCObject import +from pypsyc.objects.client import ClientUser, ClientPlace, PSYCClient +from pypsyc.objects import PSYCObject +from pypsyc.center import ClientCenter + +from pypsyc import netLocation, parsetext +#from psycObjects import PUser, PPlace, PClient, PServer + +import extras +import asyncore, sys, os + +ID_ABOUT = 1002 +ID_CONNECT = 1001 +ID_DISCONNECT = 10011 +ID_SAY = 3300 +ID_NOTEBOOK = 3301 +ID_STATUS = 9901 +ID_MENU = 9902 +ID_BUDDY_LIST = 9903 +ID_BUDDY_LIST_DK = 990301 +ID_EXIT = 1099 +ID_CLOSECHATNOTEBOOK = 2099 +ID_CLOSECHATNOTE = 2098 + + +class wxObject: + def __init__(self, parent, psyc_parent = None): + """ basic display object """ + if not psyc_parent: print 'WARNING: no psyc_parent set, this could be a problem' + self.netname = self.psyc_parent.netname + def append1(self, text): + """ use this to append multi/single line text""" + pass + + +class wxPTab(wx.Panel): + def __init__(self, parent, psyc_parent = None): + """ all das ausehen usw """ + wx.Panel.__init__(self, parent, -1, style=wx.NO_BORDER) + # we use logic_parent to call the functions pypsyc provides + # gui stuff in the wx.Blah objects, psyc stuff in the PBlah objects + if not psyc_parent: print 'WARNING: no psyc_parent set, this could be a problem' + self.psyc_parent = psyc_parent + config = self.psyc_parent.center.config + self.prompt = config['prompt'] + self.lock = 0 + self.counter = 0 + self.buffer = '' + self.text_box = wx.TextCtrl(self, -1, style=wx.NO_BORDER|wx.TE_MULTILINE|wx.TE_RICH2|wx.TE_READONLY, size=wx.DefaultSize) + self.entry_box = wx.TextCtrl(self, ID_SAY, style=wx.NO_BORDER|wx.TE_PROCESS_ENTER|wx.TE_RICH2|wx.TE_PROCESS_TAB, size=wx.DefaultSize) + + fontcolour = wx.Colour(config['fontcolour'][0], config['fontcolour'][1], config['fontcolour'][2]) + bgcolour = wx.Colour(config['bgcolour'][0], config['bgcolour'][1], config['bgcolour'][2]) + points = self.text_box.GetFont().GetPointSize() # get the current size + f = wx.Font(points, wx.MODERN, wx.NORMAL, wx.BOLD, False) + if os.name == 'nt': + self.text_box.SetDefaultStyle(wx.TextAttr(fontcolour, bgcolour, f)) + self.entry_box.SetDefaultStyle(wx.TextAttr(fontcolour, bgcolour, f)) + self.entry_box.SetBackgroundColour(bgcolour) + self.text_box.SetBackgroundColour(bgcolour) + self.SetBackgroundColour(bgcolour) + if os.name == 'posix': + self.text_box.SetDefaultStyle(wx.TextAttr(wx.NullColour, wx.NullColour, f)) + self.entry_box.SetDefaultStyle(wx.TextAttr(wx.NullColour, wx.NullColour, f)) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.text_box, 1, wx.EXPAND) + sizer.Add(self.entry_box, 0, wx.EXPAND) + self.SetSizer(sizer) + + #print 'wx.PTab: ' + str(dir(self)) + wx.EVT_TEXT_ENTER(self, ID_SAY, self.input) + + def input(self, event): + text = event.GetString() + text = text.lstrip() # only strip whites from the beginning + mc = '' + vars = {} + data = '' + vars['_target'] = self.psyc_parent.netname # eigentlich is target immer netname + if text != '' and text[0] == '/': + # houston we have a command + if text.startswith("/join"): # and text.__len__() > 16: + mc = '_request_enter' + vars['_target'] = text[6:] + #elif text.startswith("/part"): + # mc = '_request_leave' + # vars['_target'] = text[6:] + elif text.startswith('/password'): + mc = '_set_password' + t = text.split('/password') + if t[1] != '' and t[1] != ' ': + vars['_password'] = t[1] + vars['_target'] = self.psyc_parent.center.config['uni'] + else: + self.append1('Usage: /password ') + return + elif text.startswith('/connect'): + self.psyc_parent.center.connect(text[9:]) + self.append1('connecting to: ' + text[9:]) + self.entry_box.SetValue('') + return + elif text.startswith('/retrieve'): + mc = '_request_retrieve' + vars['_target'] = self.psyc_parent.center.config['uni'] + elif text.startswith('/store'): + mc = '_request_store' + vars['_target'] = self.psyc_parent.center.config['uni'] + vars['_storic'] = text[7:] + data = text[7:] + else: + vars['_target'] = self.psyc_parent.center.config['uni'] + #vars['_source'] + mc = '_request_execute' + data = text + + elif text!= '' and text.startswith('##debug'): + self.entry_box.SetValue('') + self.append1(str(getattr(self.psyc_parent.center, text[8:]))) + return + + elif text != '' and text[0] == '#': + self.append1(text) + self.entry_box.SetValue('') + return + + else: + mc = '_message_public' + try: + text2 = text.decode('iso-8859-15') + except: + text2 = text + print 'unicode' + data1 = text2.encode('iso-8859-1') + data = data1 + # am ende einfach wegschicken das fertige paket + self.entry_box.SetValue('') + #print_psyc(mmp, psyc, mc, data, 'place sending') + self.psyc_parent.sendmsg(vars, mc, data) + + def append1(self, line): + """ use this to append multi/single line text""" + if type(line) == type(u'öäü'): + try: + line = line.encode('iso-8859-1') + except: + print 'waahh' + if os.name == 'posix': + if self.lock == 0: + self.lock = 1 + if self.buffer != '': + self.text_box.AppendText('buffered: ' + self.buffer) + self.buffer = '' + for linex in line.split('\n'): + self.text_box.AppendText(self.prompt.encode('iso-8859-15') + linex) + self.text_box.AppendText('\n') + self.psyc_parent.center.Yield() + self.lock = 0 + else: + self.buffer += line + '\n' + elif os.name == 'nt': + # AppendText() doesn't seem to do the right thing in windows + for linex in line.split('\n'): + self.text_box.WriteText(self.prompt.encode('iso-8859-15') + linex) + self.text_box.WriteText('\n') + # we should find out how many 'lines' we write and scroll the right + # amount instead of scrolling 10000 at once XXX + self.text_box.ScrollLines(10000) + self.psyc_parent.center.Yield() + + def append(self, text): + """ use this for more then one line USE append1() this is obsolete!! """ + # is this broken? do we have to do the same as in append1()? + lines = text.split('\n') + for line in lines: + self.text_box.AppendText(self.prompt + line + '\n') + #print self.netname + ': ' + str(text) + + +class wxPFrame(wx.Frame): + def __init__(self, parent = None, psyc_parent = None, title = 'pypsyc-frame', pos = wx.DefaultPosition, size = wx.Size(920, 570), style = wx.DEFAULT_FRAME_STYLE): + wx.Frame.__init__(self, None, -1, title, size=size) + # do we need it here? wx.Frame is only supposed to be a container + # and shouldn't do that much -> only here to be flexible + # we use logic_parent to call the functions pypsyc provides + # gui stuff in the wx.Blah objects, psyc stuff in the PBlah objects + if not psyc_parent: print 'WARNING: no psyc_parent set, this could be a problem' + self.psyc_parent = psyc_parent + self.notebook = wx.Notebook(self, -1, style=0) + self.CreateStatusBar() + self.SetStatusText("welcome to pypsyc") + self.Show() + + def addTab(self, tab): + tab.create_display(parent=self.notebook) + self.notebook.AddPage(tab.display['default'], str(tab.netname), 1) + + +class wxPPlace(wxPTab): + def __init__(self, parent, psyc_parent = None): + wxPTab.__init__(self, parent=parent, psyc_parent=psyc_parent) + self.netname = self.psyc_parent.netname + + +class wxPUser(wxPTab): + def __init__(self, parent, psyc_parent = None): + wxPTab.__init__(self, parent=parent, psyc_parent=psyc_parent) + self.netname = self.psyc_parent.netname + + +class wxPClient(wx.Frame): + def __init__(self, parent = None, title = 'pypsyc', psyc_parent = None, pos = wx.DefaultPosition, size = wx.Size(100, 400), style = wx.DEFAULT_FRAME_STYLE): + wx.Frame.__init__(self, None, -1, title, size=size) + if not psyc_parent: print 'WARNING: no psyc_parent set, this could be a problem' + self.psyc_parent = psyc_parent + + self.CreateStatusBar() + self.SetStatusText("welcome to pypsyc") + self.BuddyList = wx.ListCtrl(self, 2222, style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.SUNKEN_BORDER) + self.BuddyList.InsertColumn(0, "ST") + self.BuddyList.InsertColumn(1, "Nick")# , wx.LIST_FORMAT_RIGHT) + self.BuddyList.SetColumnWidth(0, 20) + + self.status = wx.ComboBox(self, ID_STATUS, "", choices=["Offline", "Online", "Away"], size=(150,-1), style=wx.CB_DROPDOWN) + self.menu_button = wx.Button( self, ID_MENU, 'pypsyc') + self.exit_button = wx.Button( self, ID_EXIT, 'exit') + self.con_menu = wx.BoxSizer(wx.HORIZONTAL) + self.con_menu.Add(self.menu_button, 1, wx.ALIGN_BOTTOM) + self.con_menu.Add(self.exit_button, 1, wx.ALIGN_BOTTOM) + + sizer = wx.FlexGridSizer(3, 0 , 0,0) + sizer.Add(self.BuddyList, 1, wx.GROW) + sizer.Add(self.con_menu, 1,wx.GROW) + sizer.Add(self.status, 1,wx.GROW) + sizer.AddGrowableRow(0) + sizer.AddGrowableCol(0) + # do something so that the buttons don't vanish in a too small window + # this is h4x0r-style but does the job at the moment + sizer.SetItemMinSize(self.BuddyList, 30, 10) + sizer.SetMinSize(wx.Size(200,280)) + self.SetSizer(sizer) + self.SetAutoLayout(True) + self.Show() diff --git a/in-silico/gui/extras.py b/in-silico/gui/extras.py new file mode 100644 index 0000000..b823456 --- /dev/null +++ b/in-silico/gui/extras.py @@ -0,0 +1,66 @@ +# -*- coding: latin-1 -*- + +def print_psyc(vars, mc, data, caller = ''): + print '------ ' + str(caller) + print '-- mc:' + print mc + print '-- vars:' + print str(vars.items()) + print '-- data:' + print str([data]) + print '------' + + +class Context: + class __impl: + #from extras import Config + #def __init__(self): + # self.config = Config() + # self.hust = 'hallo garrit' + hust = 'hallo garrit' + def spam(self): + return id(self) + __instance = __impl() + + def __getattr__(self, attr): + return getattr(self.__instance, attr) + + def __setattr__(self, attr, value): + return setattr(self.__instance, attr, value) + + +class DevNull: + def __init__(self): + pass + def write(self, text): + pass + +class Config(dict): + def __init__(self, file = None): + self[u'uni'] = u'psyc://ve.symlynx.com/~betatim' + self[u'password'] = u'tim0914' + self[u'action'] = u'brabbel' + self[u'bgcolour'] = (255, 236, 191) + self[u'fontcolour'] = (34, 63, 92) + self[u'fontsize'] = 8 + self[u'prompt'] = u'* ' + + +class Display(dict): + """ this dict like object organises multiple displays """ + def __init__(self, display = None): + if display: + self['default'] = display + + def append1(self, text): + self['default'].append1(text) + + t = """def __setitem__(key = None, item = None): + if key and item: + dict.__setitem__(key, item) + elif item: + if self['default']: + print 'Overwrite default display explicitly' + else: + self['default'] = item""" + diff --git a/in-silico/gui/psycObjects.py b/in-silico/gui/psycObjects.py new file mode 100644 index 0000000..9929160 --- /dev/null +++ b/in-silico/gui/psycObjects.py @@ -0,0 +1,297 @@ +# -*- coding: latin-1 -*- + +#from wxPython.wx import * +import wx +# XXX Please feel free to modify and vershclimmbesser this piece of code +# especially the bits marked with XXX +##from pypsyc.objects.PSYCObject import +from pypsyc.objects.client import ClientUser, ClientPlace, PSYCClient +from pypsyc.objects import PSYCObject +from pypsyc.center import ClientCenter + +from pypsyc import netLocation, parsetext, UNL2Location +from displays import wxPFrame, wxPTab, wxPPlace, wxPUser, wxPClient +import asyncore, sys, os +import extras + +class PObject(PSYCObject): + def __init___(self, netname, center): + PSYCObject.__init__(self, netname, center) + self.context = extras.Context() + self.display = extras.Display() + self.queue = [] # a place to store pakets + + def create_display(self, display=None, parent=None, name = 'default'): + """ create a new display for this object """ + # display := display object + # parent := if we don't have a display object we need to create one + # and perhaps need a parent + # name := the display needs a name so we can access it later + if name == 'default': + if self.display['default']: + print 'there is already a default display, \ + if you want a new display give it a unique name' + return + else: + if display: + # a display has to have some basic functionality + self.display[name] = display + else: + # we're a bit confused now because we can't know what sort of + # display to create, but perhas someone comes up with a idea later + print 'HELP!' + + def msg(self, vars, mc, data, caller): + PSYCObject.msg(self, vars, mc, data, caller) + # store things until we know what do with them + f = (vars, mc, data, caller) + self.queue.append(f) + + +class wxCenter(wx.App, ClientCenter): + def __init__(self): #vllt noch cmd line args uebergeben or so + wx.App.__init__(self, 0) + #self.run() + + def OnInit(self): + ClientCenter.__init__(self) + self.context = extras.Context() + self.context.config = extras.Config() + self.config = self.context.config + + self.frame = wxPFrame(psyc_parent=self) # nen frame um die tabs aufzuheben + self.timer = wx.PyTimer(self.socket_check) + self.timer.Start(100) # alle 100 ms + return True + + def socket_check(self): + asyncore.poll(timeout=0.0) # das sollte vllt klappen + + def run(self): + puni = UNL2Location(self.config['uni']) + self.client = PClient(puni, self) + self.create_server_place(netLocation(self.config['uni'])) + # XXX extremly buggy here!! + #self.create_server_place('psyc://adamantine.fippo.int') + + self.default_connection = netLocation(self.config['uni']) + self.client.online() + self.client.create_display(self) + self.SetTopWindow(self.client.display['default']) + self.client.display['default'].Show() + + self.MainLoop() + + def sendmsg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, 'center sending') + ClientCenter.sendmsg(self, vars, mc, data, caller) + + def set_default_connection(self, uni): + #netLocation(self.uni) + print 'set_default_connect: ' + netLocation(uni) + self.default_connection = netLocation(uni) + + def msg(self, vars, mc, data, caller): + source = vars['_context'] or vars['_source'] + if not ClientCenter.msg(self, vars, mc, data, caller): + # create a psyc object of unknown type + #self.create_psyc_object(source) + #print '\n\n !!WE ARE IN TROUBLE!!\n\n\n' + print '' + + def create_psyc_object(self, netname): + # this could do some better guessing about the type + self.create_server_place(netname) + # XXX + + def create_user(self, netname): + t = PUser(netname, self) + #print 'i am a USER and my name is: ' + netname + ' / ' + netLocation(netname) + self.frame.addTab(t) + return t + + def create_place(self, netname): + t = PPlace(netname, self) + #print 'i am a PLACE and my name is: ' + netname + ' / ' + netLocation(netname) + self.frame.addTab(t) + return t + + def create_server_place(self, netname): + t = PServer(netname, self) + #print 'i am a SERVER and my name is: ' + netname + ' / ' + netLocation(netname) + self.frame.addTab(t) + self.client.create_display(name='server', display=t.display['default']) + #sys.stdout = extras.DevNull() + return t + +class PPlace(ClientPlace): + def __init__(self, netname, center): + ClientPlace.__init__(self, netname, center) + self.display = extras.Display() + print 'i am a PLACE and my name is: ' + self.netname + ' / ' + netLocation(self.netname) + + def create_display(self, parent = None, name = 'default', display = None): + if name == 'default': + self.display['default'] = wxPPlace(parent=parent, psyc_parent=self) + else: + self.display[name] = display + + def msg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, 'place') + #print 'ddd' + str(type(data)) + parsedtext = parsetext(vars, mc, data) + #print dir(parsedtext) , type(parsedtext) + #parsedtext = parsedtext.encode('iso-8859-15') + #print type(parsedtext) + if mc == '_message_public': + line = u'' + if vars['_nick']: + line += vars['_nick'] + else: + line += vars['_source'] + + if vars['_action']: line += ' ' + vars['_action'] + '>' + else: line += '>' + try: + line += ' ' + parsedtext.decode('iso-8859-15') + except: + line += ' ' + parsedtext + self.display.append1(line) + elif mc == '_message_public_question': + line = u'' + line += vars['_nick'] + if vars['_action']: line += ' ' + vars['_action'] + '>' + else: line += ' fragt>' + try: + line += ' ' + parsedtext.decode('iso-8859-15') + except: + line += ' ' + parsedtext + self.display.append1(line) + elif mc.startswith('_status_place_topic'): + self.display.append1('TOPIC: ' + parsedtext) + else: + self.display.append1(parsedtext) + + +class PUser(ClientUser): + def __init__(self, netname, center): + ClientUser.__init__(self, netname, center) + self.display = extras.Display() + print 'i am a USER and my name is: ' + self.netname + ' / ' + netLocation(self.netname) + + def create_display(self, parent = None, name = 'default', display = None): + if name == 'default': + self.display['default'] = wxPUser(parent=parent, psyc_parent=self) + else: + self.display[name] = display + + def msg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, caller) + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + if mc == '_message_private': + self.display.append1(parsedtext) + else: + self.display.append1(parsedtext) + + +class PServer(PSYCObject): + def __init__(self, netname, center): + PSYCObject.__init__(self, netname, center) + print 'registered server' + #print 'PServer: ' + str(dir(self)) + self.display = extras.Display() + print 'i am a SERVER and my name is: ' + self.netname + ' / ' + netLocation(self.netname) + + def create_display(self, parent = None, name = 'default', display = None): + if name == 'default': + self.display['default'] = wxPPlace(parent=parent, psyc_parent=self) + else: + self.display[name] = display + + def msg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, 'server') + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + #self.center.Yield() + if mc == '_notice_circuit_established' and vars['_source'] == netLocation(self.center.config['uni']): + mc = '_request_link' + vars = {} + data ='' + vars['_target'] = self.center.config['uni'] + self.sendmsg(vars, mc, data) + self.display.append1(parsedtext) + else: + self.display.append1(parsedtext) + + def write(self, text): + """ redirected stdout """ + # we have problems if text contains \n and with print adding an extra \n + lines = text.split('\n') + for line in lines: + if line == '': + return + else: + self.display.append1('printed: ' + line) + + def sendmsg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, 'server sending...') + PSYCObject.sendmsg(self, vars, mc, data, caller) + + +class PClient(PSYCClient): + def __init__(self, netname, center): + PSYCClient.__init__(self, netname, center) + self.display = extras.Display() + #self.extra_display = {} # XXX + #self.create_display(parent=None) + print 'default_conmnect is --> ' + self.center.default_connection + print 'i am a CLIENT and my name is: ' + self.netname + ' / ' + netLocation(self.netname) + + def create_display(self, parent = None, name = 'default', display = None): + if name == 'default': + self.display[name] = wxPClient(parent=parent, psyc_parent=self) + else: + if self.display.has_key(name): + # XXX tempory hack for multi server connect + print 'HACKING GOING ON!' + return + else: + self.display[name] = display + + def set_display(self, which, display): + self.display[which] = display + + def msg(self, vars, mc, data, caller = None): + extras.print_psyc(vars, mc, data, 'client') + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + if mc == '_query_password': + if self.center.config['password'] != '': + mc = '_set_password' + vars = {'_password' : self.center.config['password']} + vars['_target'] = self.netname + data ='' + self.sendmsg(vars, mc, data) + else: + self.display['server'].append1("Please enter your password or choose a different nick if you don't know the password") + self.display['server'].entry_box.SetValue('/password ') + elif mc == '_status_friends': + self.display['server'].append1(parsedtext) + + elif mc == '_error_invalid_password': + self.display['server'].append1(parsedtext) + self.display['server'].append1("Please enter your password or choose a different nick if you don't know the password") + self.display['server'].entry_box.SetValue('/password ') + else: + self.display['server'].append1(parsedtext) + + def online(self): + self.center.connect(self.center.default_connection) + #mc = '_request_link' + #mmp = {} + #psyc = {} + #data ='' + #mmp['_target'] = self.netname + + #self.sendmsg(mmp, psyc, mc, data) diff --git a/in-silico/gui/wx.py b/in-silico/gui/wx.py new file mode 100644 index 0000000..679dfa8 --- /dev/null +++ b/in-silico/gui/wx.py @@ -0,0 +1,450 @@ +# -*- coding: latin-1 -*- +from wxPython.wx import * + +# XXX Please feel free to modify and vershclimmbesser this piece of code +# especially the bits marked with XXX +##from pypsyc.objects.PSYCObject import +from pypsyc.objects.PSYCObject import ClientUser, ClientPlace, PSYCClient, PSYCObject +from pypsyc.center import ClientCenter + +from pypsyc import netLocation, parsetext + +import asyncore, sys, os + +# ich glaub ich weiss jetzt wofuer die hier toll sind +# !!! +# this list isn't up-to-date anymore, stolen from the old gui code +# !!! +ID_ABOUT = 1002 +ID_CONNECT = 1001 +ID_DISCONNECT = 10011 +ID_SAY = 3300 +ID_NOTEBOOK = 3301 +ID_STATUS = 9901 +ID_MENU = 9902 +ID_BUDDY_LIST = 9903 +ID_BUDDY_LIST_DK = 990301 +ID_EXIT = 1099 +ID_CLOSECHATNOTEBOOK = 2099 +ID_CLOSECHATNOTE = 2098 + +def opj(path): + """Convert paths to the platform-specific separator""" + return apply(os.path.join, tuple(path.split('/'))) + +def print_psyc(vars, mc, data, caller = ''): + print '------ ' + str(caller) + print '-- mc:' + print mc + print '-- vars:' + print str(vars.items()) + print '-- data:' + print str([data]) + print '------' + + +class DevNull: + def write(self, text): + return + +class wxCenter(wxApp, ClientCenter): + def __init__(self): #vllt noch cmd line args uebergeben or so + wxApp.__init__(self, 0) + self.run() + + def OnInit(self): + ClientCenter.__init__(self) + self.config = {} # = { 'uin' : 'psyc://blah/~user', 'foo': 'bar'} + self.config[u'uni'] = u'psyc://ve.symlynx.com/~betatim3' + self.config[u'password'] = u'tim' + self.config[u'action'] = u'brabbel' + self.config[u'bgcolour'] = (255, 236, 191) + self.config[u'fontcolour'] = (34, 63, 92) + self.config[u'fontsize'] = 8 + self.config[u'prompt'] = u'* ' + print sys.getdefaultencoding() + print os.name + + #print self.config + self.frame = wxPFrame(logic_parent=self) # nen frame um die tabs aufzuheben + self.client = PClient(self.config['uni'], self) + self.create_server_place(netLocation(self.config['uni'])) # eignetlich is es kein place aber naja + + self.default_connection = netLocation(self.config['uni']) + self.client.online() + self.client.create_display(self) + self.SetTopWindow(self.client.display) + self.client.display.Show() + + self.timer = wxPyTimer(self.socket_check) + self.timer.Start(100) # alle 100 ms + return True + + def input(self, event): + print event.GetString() + event.Skip() + + def socket_check(self): + asyncore.poll(timeout=0.0) # das sollte vllt klappen + + def run(self): + self.MainLoop() + + def sendmsg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, 'center sending') + ClientCenter.sendmsg(self, vars, mc, data, caller) + + def set_default_connection(self, uni): + #netLocation(self.uni) + print 'set_default_connect: ' + netLocation(uni) + self.default_connection = netLocation(uni) + + def create_user(self, netname): + #print "this should create a user" + t = PUser(netname, self) + # nicht tab.display damit wir netname haben ,]] + # auch wenn es extrem strange ist wir uebergeben + self.frame.addTab(t) + return t + + def create_place(self, netname): + #print "this should create a room" + t = PPlace(netname, self) + self.frame.addTab(t) # same here + return t + + def create_server_place(self, netname): + t = PServer(netname, self) + self.frame.addTab(t, extra_display='server') # same here + sys.stdout = t + return t + + +class wxPTab(wxPanel): + def __init__(self, parent, logic_parent = None): + """ all das ausehen usw """ + wxPanel.__init__(self, parent, -1, style=0) + # we use logic_parent to call the functions pypsyc provides + # gui stuff in the wxBlah objects, psyc stuff in the PBlah objects + if not logic_parent: print 'WARNING: no logic_parent set, this could be a problem' + self.logic_parent = logic_parent + config = self.logic_parent.center.config + self.prompt = config['prompt'] + self.lock = 0 + self.counter = 0 + self.buffer = '' + self.text_box = wxTextCtrl(self, -1, style=wxTE_MULTILINE|wxTE_RICH2|wxTE_READONLY, size=wxDefaultSize) + self.entry_box = wxTextCtrl(self, ID_SAY, style=wxTE_PROCESS_ENTER|wxTE_RICH2|wxTE_PROCESS_TAB, size=wxDefaultSize) + + fontcolour = wxColour(config['fontcolour'][0], config['fontcolour'][1], config['fontcolour'][2]) + bgcolour = wxColour(config['bgcolour'][0], config['bgcolour'][1], config['bgcolour'][2]) + self.entry_box.SetBackgroundColour(bgcolour) + self.text_box.SetBackgroundColour(bgcolour) + points = self.text_box.GetFont().GetPointSize() # get the current size + f = wxFont(points, wxMODERN, wxNORMAL, wxBOLD, False) + self.text_box.SetDefaultStyle(wxTextAttr(fontcolour, bgcolour, f)) + self.entry_box.SetDefaultStyle(wxTextAttr(fontcolour, bgcolour, f)) + self.SetBackgroundColour(bgcolour) + + sizer = wxBoxSizer(wxVERTICAL) + sizer.Add(self.text_box, 1, wxEXPAND) + sizer.Add(self.entry_box, 0, wxEXPAND) + self.SetSizer(sizer) + + #print 'wxPTab: ' + str(dir(self)) + EVT_TEXT_ENTER(self, ID_SAY, self.input) + + def input(self, event): + text = event.GetString() + text = text.lstrip() # only strip whites from the beginning + mc = '' + vars = {} + data = '' + vars['_target'] = self.logic_parent.netname # eigentlich is target immer netname + if text != '' and text[0] == '/': + # houston we have a command + if text.startswith("/join"): # and text.__len__() > 16: + mc = '_request_enter' + vars['_target'] = text[6:] + elif text.startswith("/part"): + mc = '_request_leave' + vars['_target'] = text[6:] + elif text.startswith('/password'): + mc = '_set_password' + t = text.split('/password') + if t[1] != '' and t[1] != ' ': + vars['_password'] = t[1] + vars['_target'] = self.logic_parent.center.config['uni'] + else: + self.append1('Usage: /password ') + return + elif text.startswith('/retrieve'): + mc = '_request_retrieve' + vars['_target'] = self.logic_parent.center.config['uni'] + else: + mc = '_request_execute' + data = text + + elif text != '' and text[0] == '#': + self.append1(text) + self.entry_box.SetValue('') + return + + else: + mc = '_message_public' + try: + text2 = text.decode('iso-8859-15') + except: + text2 = text + print 'unicode' + data1 = text2.encode('iso-8859-1') + data = data1 + # am ende einfach wegschicken das fertige paket + self.entry_box.SetValue('') + #print_psyc(mmp, psyc, mc, data, 'place sending') + self.logic_parent.sendmsg(vars, mc, data) + + def append1(self, line): + """ use this to append multi/single line text""" + if type(line) == type(u'öäü'): + line = line.encode('iso-8859-15') + if os.name == 'posix': + if self.lock == 0: + self.lock = 1 + if self.buffer != '': + self.text_box.AppendText('buffered: ' + self.buffer) + self.buffer = '' + for linex in line.split('\n'): + self.text_box.AppendText(self.prompt.encode('iso-8859-15') + linex) + self.text_box.AppendText('\n') + self.logic_parent.center.Yield() + self.lock = 0 + else: + self.buffer += line + '\n' + elif os.name == 'nt': + # AppendText() doesn't seem to do the right thing in windows + for linex in line.split('\n'): + self.text_box.WriteText(self.prompt.encode('iso-8859-15') + linex) + self.text_box.WriteText('\n') + # we should find out how many 'lines' we write and scroll the right + # amount instead of scrolling 10000 at once XXX + self.text_box.ScrollLines(10000) + self.logic_parent.center.Yield() + + def append(self, text): + """ use this for more then one line USE append1() this is obsolete!! """ + # is this broken? do we have to do the same as in append1()? + lines = text.split('\n') + for line in lines: + self.text_box.AppendText(self.prompt + line + '\n') + #print self.netname + ': ' + str(text) + + +class wxPFrame(wxFrame): + def __init__(self, parent = None, logic_parent = None, title = 'pypsyc-frame', pos = wxDefaultPosition, size = wxSize(920, 570), style = wxDEFAULT_FRAME_STYLE): + wxFrame.__init__(self, None, -1, title, size=size) + # do we need it here? wxFrame is only supposed to be a container + # and shouldn't do that much -> only here to be flexible + # we use logic_parent to call the functions pypsyc provides + # gui stuff in the wxBlah objects, psyc stuff in the PBlah objects + if not logic_parent: print 'WARNING: no logic_parent set, this could be a problem' + self.logic_parent = logic_parent + self.notebook = wxNotebook(self, -1, style=0) + self.CreateStatusBar() + self.SetStatusText("welcome to pypsyc") + self.Show() + + def addTab(self, tab,extra_display = None): + tab.create_display(self.notebook) + if extra_display: + self.logic_parent.client.set_display(extra_display, tab.display) + self.notebook.AddPage(tab.display, str(tab.netname), 1) + + +class wxPPlace(wxPTab): + def __init__(self, parent, logic_parent = None): + wxPTab.__init__(self, parent=parent, logic_parent=logic_parent) + self.netname = self.logic_parent.netname + + +class wxPUser(wxPTab): + def __init__(self, parent, logic_parent = None): + wxPTab.__init__(self, parent=parent, logic_parent=logic_parent) + self.netname = self.logic_parent.netname + + +class wxPClient(wxFrame): + def __init__(self, parent = None, title = 'pypsyc', logic_parent = None, pos = wxDefaultPosition, size = wxSize(100, 400), style = wxDEFAULT_FRAME_STYLE): + wxFrame.__init__(self, None, -1, title, size=size) + # we use logic_parent to call the functions pypsyc provides + # gui stuff in the wxBlah objects, psyc stuff in the PBlah objects + if not logic_parent: print 'WARNING: no logic_parent set, this could be a problem' + self.logic_parent = logic_parent + + self.CreateStatusBar() + self.SetStatusText("welcome to pypsyc") + self.BuddyList = wxListCtrl(self, 2222, style=wxLC_REPORT|wxLC_SINGLE_SEL|wxSUNKEN_BORDER) + self.BuddyList.InsertColumn(0, "ST") + self.BuddyList.InsertColumn(1, "Nick")# , wxLIST_FORMAT_RIGHT) + self.BuddyList.SetColumnWidth(0, 20) + + self.status = wxComboBox(self, ID_STATUS, "", choices=["Offline", "Online", "Away"], size=(150,-1), style=wxCB_DROPDOWN) + self.menu_button = wxButton( self, ID_MENU, 'pypsyc') + self.exit_button = wxButton( self, ID_EXIT, 'exit') + self.con_menu = wxBoxSizer(wxHORIZONTAL) + self.con_menu.Add(self.menu_button, 1, wxALIGN_BOTTOM) + self.con_menu.Add(self.exit_button, 1, wxALIGN_BOTTOM) + + sizer = wxFlexGridSizer(3, 0 , 0,0) + sizer.Add(self.BuddyList, 1, wxGROW) + sizer.Add(self.con_menu, 1,wxGROW) + sizer.Add(self.status, 1,wxGROW) + sizer.AddGrowableRow(0) + sizer.AddGrowableCol(0) + # do something so that the buttons don't vanish in a too small window + # this is h4x0r-style but does the job at the moment + sizer.SetItemMinSize(self.BuddyList, 30, 10) + sizer.SetMinSize(wxSize(200,280)) + self.SetSizer(sizer) + self.SetAutoLayout(true) + self.Show() + + +class PPlace(ClientPlace): + def __init__(self, netname, center): + ClientPlace.__init__(self, netname, center) + self.display = None + + def create_display(self, parent): + self.display = wxPPlace(parent=parent, logic_parent=self) + + def msg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, 'place') + #print 'ddd' + str(type(data)) + parsedtext = parsetext(vars, mc, data) + #print dir(parsedtext) , type(parsedtext) + #parsedtext = parsedtext.encode('iso-8859-15') + #print type(parsedtext) + if mc == '_message_public': + line = u'' + line += vars['_nick'] + if vars['_action']: line += ' ' + vars['_action'] + '>' + else: line += '>' + try: + line += ' ' + parsedtext.decode('iso-8859-15') + except: + line += ' ' + parsedtext + self.display.append1(line) + elif mc.startswith('_status_place_topic'): + self.display.append1('TOPIC: ' + parsedtext) + else: + self.display.append1(parsedtext) + + +class PUser(ClientUser): + def __init__(self, netname, center): + ClientUser.__init__(self, netname, center) + self.display = None + + def create_display(self, parent): + self.display = wxPUser(parent=parent, logic_parent=self) + + def msg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, caller) + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + if mc == '_message_public': + self.display.append1(parsedtext) + else: + self.display.append1(parsedtext) + + +class PServer(PSYCObject): + def __init__(self, netname, center): + PSYCObject.__init__(self, netname, center) + print 'registered server' + #print 'PServer: ' + str(dir(self)) + self.display = None + + def create_display(self, parent): + self.display = wxPPlace(parent=parent, logic_parent=self) + + def msg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, 'server') + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + #self.center.Yield() + if mc == '_notice_circuit_established': + mc = '_request_link' + vars = {} + data ='' + vars['_target'] = self.center.config['uni'] + self.sendmsg(vars, mc, data) + self.display.append1(parsedtext) + else: + self.display.append1(parsedtext) + + def write(self, text): + """ redirected stdout """ + # we have problems if text contains \n and with print adding an extra \n + lines = text.split('\n') + for line in lines: + if line == '': + return + else: + self.display.append1('printed: ' + line) + + def sendmsg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, 'server sending...') + PSYCObject.sendmsg(self, vars, mc, data, caller) + + +class PClient(PSYCClient): + def __init__(self, netname, center): + PSYCClient.__init__(self, netname, center) + self.display = None + self.extra_display = {} # XXX + #self.create_display(parent=None) + print 'default_conmnect is --> ' + self.center.default_connection + + def create_display(self, parent): + self.display = wxPClient(parent=parent) + def set_display(self, which, display): + self.extra_display[which] = display + + + def msg(self, vars, mc, data, caller = None): + print_psyc(vars, mc, data, 'client') + parsedtext = parsetext(vars, mc,data) + #parsedtext = parsedtext.encode('iso-8859-15') + if mc == '_query_password': + if self.center.config['password'] != '': + mc = '_set_password' + vars = {'_password' : self.center.config['password']} + vars['_target'] = self.netname + data ='' + self.sendmsg(vars, mc, data) + else: + self.extra_display['server'].append1("Please enter your password or choose a different nick if you don't know the password") + self.extra_display['server'].entry_box.SetValue('/password ') + elif mc == '_status_friends': + self.extra_display['server'].append1(parsedtext) + + elif mc == '_error_invalid_password': + self.extra_display['server'].append1(parsedtext) + self.extra_display['server'].append1("Please enter your password or choose a different nick if you don't know the password") + self.extra_display['server'].entry_box.SetValue('/password ') + else: + self.extra_display['server'].append1(parsedtext) + + def online(self): + self.center.connect(self.center.default_connection) + #mc = '_request_link' + #mmp = {} + #psyc = {} + #data ='' + #mmp['_target'] = self.netname + + #self.sendmsg(mmp, psyc, mc, data) + + diff --git a/in-silico/test.py b/in-silico/test.py new file mode 100644 index 0000000..3247f00 --- /dev/null +++ b/in-silico/test.py @@ -0,0 +1,8 @@ +from gui.psycObjects import wxCenter + + +if __name__ == '__main__': + center = wxCenter() + center.run() + #center = wx.wxCenter() + #center.heartbeat() diff --git a/mjacob/TODO.txt b/mjacob/TODO.txt new file mode 100644 index 0000000..f38a9e2 --- /dev/null +++ b/mjacob/TODO.txt @@ -0,0 +1,8 @@ +-"AuthModul" muss erweitert (Passwort) und aus twisted_client.py verschoben +werden +-Ereignisse (bzw. alles außer Nachrichten) werden noch nicht angezeigt +-private messages und in andere Räume joinen funktionieren noch nicht +-Einrichtungsassistent oder etwas in die Richtung wäre nicht schlecht +-Schließen funktioniert nicht (man muss im Moment /quit in den Client, Ctrl-C +im Konsolenfenster eingeben und Fenster schließen) +-noch viel mehr... diff --git a/mjacob/UI/__init__.py b/mjacob/UI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/UI/__init__.pyc b/mjacob/UI/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..054c29d7c177160514a59a2c49781911928149e1 GIT binary patch literal 115 zcmd1(#LHFxz|}pO0SXv_v;z k$@;ljiOKm%`k|is@$s2?nI-Y@dIgmwAQS9C1{8w`064A`u>b%7 literal 0 HcmV?d00001 diff --git a/mjacob/UI/wx_.py b/mjacob/UI/wx_.py new file mode 100755 index 0000000..10a4e4a --- /dev/null +++ b/mjacob/UI/wx_.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import wx + +from base import Module +from lib import psyctext +from twisted_client import install_wx + + +class Tab(wx.Panel): + def __init__(self, parent, name): + self.name = name + wx.Panel.__init__(self, parent, -1) + self.textctrl = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE|wx.TE_READONLY) + self.users = [] + + def OnResize(event): + self.textctrl.SetSize(event.GetSize()) + self.Bind(wx.EVT_SIZE, OnResize, self) + + +class MainWindow(Module, wx.Frame): + methods = ['_message*'] + def __init__(self, ui): + wx.Frame.__init__(self, None, -1, size = (800, 600)) + + self.Bind(wx.EVT_CLOSE, self.OnClose, self) + + self.ui = ui + + self.tabs = {} + splitter = wx.SplitterWindow(self, -1) + + self.pl = wx.Panel(splitter, -1) + sizer = wx.GridBagSizer(0, 0) + + self.nb = wx.Notebook(self.pl, -1, style = wx.NB_TOP) + sizer.Add(self.nb, (0, 0), (1, 1), wx.EXPAND) + sizer.AddGrowableCol(0) + sizer.AddGrowableRow(0) + + self.input = wx.TextCtrl(self.pl, -1) + sizer.Add(self.input, (1, 0), (1, 1), wx.EXPAND) + self.pl.SetSizerAndFit(sizer) + self.add_tab("psyc://%s" % self.ui.base.config.host) + + self.pr = wx.Panel(splitter, -1) # right panel + self.userlist = wx.ListBox(self.pr, -1, (0, 5), style = wx.SUNKEN_BORDER) + + self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnChangeTab, self.nb) + + def OnResize(event): + size = event.GetSize() + self.userlist.SetSize((size[0], size[1] - 10)) + self.pr.Bind(wx.EVT_SIZE, OnResize, self.pr) + + splitter.SplitVertically(self.pl, self.pr, self.GetSize()[0] * 0.7) + splitter.SetSashGravity(1) + + mainmenu = wx.Menu() + + menuitem = mainmenu.Append(-1, '&Connect', 'Connect to the server') + self.Bind(wx.EVT_MENU, self.OnConnect, menuitem) + + menuitem = mainmenu.Append(-1, '&Debug window', + 'Open or close debug window') + self.Bind(wx.EVT_MENU, self.OnOpenDebugWindow, menuitem) + + mainmenu.AppendSeparator() + + menuitem = mainmenu.Append(-1, '&Exit', 'Exit pyPSYC') + self.Bind(wx.EVT_MENU, self.OnClose, menuitem) + + menubar = wx.MenuBar() + menubar.Append(mainmenu, '&pyPSYC') + + self.SetMenuBar(menubar) + + self.input.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + + self.Show(True) + + def OnKeyDown(self, event): + if event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): + pagename = self.nb.GetCurrentPage().name + value = self.input.GetValue() + if value: + self.center.input(pagename, value) + self.input.SetValue('') + + event.Skip() + + def OnChangeTab(self, event): + self.userlist.Set(self.nb.GetCurrentPage().users) + + def OnConnect(self, event): + self.center.connect(self.center.config.host) + self.ui.connected = True + + def OnOpenDebugWindow(self, event): + # open or close window: + if not self.ui.debugwindow.Show(True): self.ui.debugwindow.Show(False) + + def OnClose(self, event): + pass + + def add_tab(self, name): + self.tabs[name] = Tab(self.nb, name) + self.nb.AddPage(self.tabs[name], name) + + def handle_message_public(self, packet, physsource): + self._print(packet.mmpvars['_context'], "%s: %s" % (packet.psycvars['_nick'], psyctext(packet))) + + handle_message_echo_public = handle_message_public + + def _print(self, window, text): + if window not in self.tabs: + self.add_tab(window) + self.tabs[window].textctrl.AppendText(text + '\n') + + + +class UI: + def __init__(self, base): + self.base = base + self.gui = wx.PySimpleApp() + self.mainwindow = MainWindow(self) + self.base.register_module(self.mainwindow) + install_wx(self.gui) diff --git a/mjacob/UI/wx_.pyc b/mjacob/UI/wx_.pyc new file mode 100644 index 0000000000000000000000000000000000000000..818e9cb86bfe451b9469d2c6420e94d790bb2b33 GIT binary patch literal 5468 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 literal 0 HcmV?d00001 diff --git a/mjacob/base.py b/mjacob/base.py new file mode 100755 index 0000000..6a1cd30 --- /dev/null +++ b/mjacob/base.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## base.py + + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + + +from lib.parse import MMPParser, PSYCParser +from lib.render import MMPRenderer, PSYCRenderer +from lib.packets import MMPPacket, PSYCPacket + + +debug = False + + +class Module: + methods = [] + + def register_center(self, center): + self.center = center + + def received(self, packet, physsource): + func = getattr(self, 'handle_%s' % packet.mc[1:], None) + + if func: + func(packet, physsource) + + else: + self.handle(packet, physsource) + + +class MMPCircuit: + def __init__(self, center): + self.center = center + self.mmp_parser = MMPParser() + self.mmp_parser.recv_packet = self.recv_packet + self.mmp_renderer = MMPRenderer() + + def init(self): + self.send_mmp_packet(MMPPacket()) + + def recv_packet(self, packet): + self.center.recv_mmp_packet(packet, self) + + def send_mmp_packet(self, packet): + self._send(self.mmp_renderer.render(packet)) + + +class PSYCCircuit(MMPCircuit): + def __init__(self, center): + MMPCircuit.__init__(self, center) + self.psyc_parser = PSYCParser() + self.psyc_renderer = PSYCRenderer() + + def send_psyc_packet(self, packet): + self.send_mmp_packet(self.psyc_renderer.render(packet)) + + +class Plugin: + def __init__(self, name): + self.name = name + self.p = __import__('plugins.' + self.name, (), (), [self.name]) + self.needs = self.p.needs + + def load(self): + self.instance = self.p.Module() + + +class Config: + def __init__(self, app): + self.app = app + + self.settings_folder = self.get_settings_folder() + self.configfile = self.open_configfile() + + +class BaseCenter: + type = 'base' + registered_modules = [] + handlers = {} + plugins = {} + connections = {} + + def load_plugin(self, name): + p = Plugin(name) + + for load in p.needs: + self.load_plugin(load) + + p.load() + self.register_module(p.instance) + self.plugins[name] = p + + def register_module(self, module): + for i in module.methods: + if i in self.handlers: + self.handlers[i].append(module) + + else: + self.handlers[i] = [module] + + module.register_center(self) + self.registered_modules.append(module) + + def connected(self, connection, host): + self.connections[host] = connection + self._connected(connection, host) + + def _connected(self, connection, host): + pass + + def recv_mmp_packet(self, packet, physsource): # called by circuit + raise NotImplementedError + + def recv_psyc_packet(self, packet, physsource): # called by subclass + handlers = [] + + for key, value in self.handlers.items(): + if key == packet.mc or \ + (key[-1] == '*' and packet.mc.startswith(key[:-1])): + handlers.extend(value) + + for i in handlers: + i.received(packet, physsource) + + def send_mmp_packet(self, packet, physdest): + raise NotImplemented + + def _send_psyc_packet(self, packet, physdest): + if debug: # WARNING: Do not show all packets + print """packet to %s:: +mmpvars: %s +psycvars: %s +mc: %s +text: %s +""" % (physdest, packet.mmpvars, packet.psycvars, packet.mc, packet.data) + + physdest.send_psyc_packet(packet) diff --git a/mjacob/base.pyc b/mjacob/base.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75c1c07770eb42c720d0839911db4902b73201be GIT binary patch literal 5329 zcmb7IjdC1E5$@f+e@m7WIkF8&P<|W^Ls=9A2u1|Qa-0~-SFUw|@fEJE^X{Cq*8TZ* zB&;f{s6r{8fQR96cm`eozOQF*cYU^lE!B)>dU|KN`|IxM(fB{BoxlD2tFL3pe**ly zftLLXT_SQQGV-MF$;g+!FJ9uwVIZTL^lLJzOTP}1FNX~oHKpH_QA_$Q@l0F#9Z3Qi zVw0MDD{?0CuzyaHx{?|NX+@Hzm=zg%lC%oysw8b?>=dN)lAKf0N7lWiSTnaZGMmv*7ea0klCg{CI{O|*JAc|q%<<0bWR{)8JL4yVc=~wf z@gOtTW+!$H40E%eX4o%s+g<`G<7VQ;6p*LX(NwfM7(gwE28AGFstWyJD*@DjoCfkd zkm-ha_(KhRIfL^|NDxVz6&gOs^;fJ829soDqAOXd8nk#!6WzYC18e81oT@$>sOc%+ zI93bvrW2!s;n5_vgnD*xl4aAQd2I9(w|AF%I3p3akc=VSq%>DZbuG`Ho7kksCiyD`Tpi4_AOB788V-rPsVib+l4=|MhZu#6QQlX$*k>RYE@lCf zVj2N8;8fE-kfJTzgqQlOBY zQn8OtL2~}(ylNBE9kka9@;(-wAnbjHFgyWvSuzbh$C-wsh_(sot{PJgtT&N|djAY9 zm#0gaLuR<}Ji9G+m)TM9kldwtE5X9leg#e8Zg}lRJ3zS5mzdaJ8!z7j!|USZr374H zl}8zz1}36$02ETpnHnyt(**grS&fPo4ht!$(~9tGkZ44d_m4`=WW0zFMgW%=SC6H6 zl;O={LXZ0uzR^q~-a2BTxK#?J-WqoWBm9tByvOeUJD;WU8brBzjGJB3XbbUN4qgKP zBdoj>YsLwM%^_}kS6-3q$V7>8FIQ5!ipbM?l{@fL%=HG+%PgH_b})&}cjvg# zbdcPp4Bvs~kFZ|Kt|O zWh^xnGXhV0D`tOoBDtFfb>p)SSUTb^y7g#NPH{s5w}uvwxUipY$pfw`kc(0h^OoLO ztTn;irVs2^0Hz4x$Lu~~ryl=9j_9Kfao!*F>d3|Ii{-E@=|b9FtGYb4D?z0D>TcXz z0n*MP%ve`ycNF#tTy}(^0V55wxCVa9%Bri-JuGI-dm5pP#ZWi)Z|G+rk7j| zjWp!T<+M&tUf54NpyQF@@|Lddg7$zL!G0*q!9?>krTnO3q!!NAyqasvGDuTX>MX={ zS3u7*sR z*RfT&i*8AH+Xk)pfU%`HO}vWiOt&gK?NkvDGkJp}!ry{#j+UodeTdN>#lCcIg!JF> zelDw7u3BcX-qhY1&qijYF8UFCaFSGM7!`J)jb{TxpY z7xNnTE-~DCv?Zs2ye4U<0{qisd7%2!Hof$}_)+J*+GR}@vL#%MtCgWIX78vND+oBwVGBJ;j_QP*5QUKhe+il}C&9`x| z(tVSG9fTiYuw)qBWXT|w)rUTKqDme64-oUqy6JVBU1rMivetZJPi7|kHR|U%%aHpKI6n4#MA{*VUSd zxoMO>zNU}wkfiVfbn4oq=MT3FT-fG{Ed(0? literal 0 HcmV?d00001 diff --git a/mjacob/client.py b/mjacob/client.py new file mode 100755 index 0000000..d8594de --- /dev/null +++ b/mjacob/client.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +import os + +from base import Config, BaseCenter +from lib.packets import PSYCPacket, MMPPacket +from lib import get_host, get_user + +class ClientConfig(Config): + def __init__(self, app): + Config.__init__(self, app) + self.uni_file = os.path.join(self.settings_folder, 'me') + self.clientconfig_file = os.path.join(self.settings_folder, 'pypsyc', 'config') + + #self.uni = self.get_uni() + import random + self.uni = 'psyc://beta.ve.symlynX.com/~pypsyctest' + ''.join([random.Random().choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789') for i in [0]*10]) + #self.uni = raw_input('uni?: ') + self.host = get_host(self.uni) + self.username = get_user(self.uni) + + self.ui = 'wx' + + def get_settings_folder(self): + # resolve settings folder + if os.name == 'posix': + folder = os.path.join(os.environ.get('HOME'), ".psyc") + elif os.name == 'nt': + folder = os.path.join(os.environ.get('APPDATA'), 'psyc') + else: + folder = os.path.join(os.getcwd(), 'psyc') + + # create settings folder if necessary + try: + os.mkdir(folder) + except OSError: + pass + + return folder + + def get_uni(self): + f = file(self.uni_file) + uni = f.read().strip() + f.close() + return uni + + def open_configfile(self): + return None + + +class ClientCenter(BaseCenter): + type = 'client' + + def __init__(self): + self.config = ClientConfig(self) + + def _connected(self, connection, host): + pass + + def recv_mmp_packet(self, packet, physsource): + if not packet.vars and not packet.body: + return + psyc_packet = physsource.psyc_parser.parse(packet) + self.recv_psyc_packet(psyc_packet, physsource) + + def send_psyc_packet(self, packet): + self._send_psyc_packet(packet, self.connections[self.config.host]) + + def input(self, pagename, text): + if '@' in pagename: + self.send_psyc_packet(PSYCPacket(mmpvars = {'_target': pagename}, mc = '_message_public', data = text)) + + if text.startswith('/'): + self.command(pagename, text) + + def command(self, pagename, text): + if text == '/quit' or '/bye': + self.send_psyc_packet(PSYCPacket(mmpvars = {'_target': self.config.uni}, mc = '_request_execute', data = text)) + + else: + self.send_psyc_packet(PSYCPacket(psycvars = {'_focus': pagename}, mc = '_request_input', data = text)) diff --git a/mjacob/client.pyc b/mjacob/client.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef6dffd8e1005e616c9fef8d16c7df21025049dc GIT binary patch literal 3701 zcma)9>v9`K7Cx=f)#PfMuoNk(_z_C%QdR{B_k|?3Nw`=sW0oY^qNe1Twmr5qGtsmh zi%Q@}c%XfbeF~m|C*b=|OO{X-6i#)g=k(=VzOT<|4S&11^7F6T_hOm;1^E9qS~)_O zh@6P@FfejrWKfY_MFxTN0x?NNPO37fNv|e@y7cN01#;4mK~s878MLI=f~YFCA_qp2 zn*1X2LgaC8MUuMMK&6dy=}XkM*KR^>w_TwHb#$9~c90&HD;VDGmv+-;&W`*vWxIQi zH+TE-iFIWQpF2Cd{>!i6S%=m|M|tVW*~hrFBXRYJjZEOc@{`|kr7WX zNz#-TMpmB;<)+i>jJvcl{k52}} zEH9pnN;f`zdN%p-&fU$e?Jw@#|MIJ^zuDlk&u?$Rx2@mnDJY={XYmiw?X0KisU!IZTMM{G|&m< zIj#-pMH*M+xsmK!zX+o0Uu>%BruuBt^`(;=BsN&@&zyj`(A(4tX2d+q)}-oMa=apW zLk#{hT)RiOetT`+%&y}dE%Guw(?H*Uu(MrKw4r3ClbLf|d1rTb>&|y~bQR~?8snER zqaxxEGMR)IDdG;Bou;EaQ&fSbVm{>#F^4FAnyCKaNs^9q&x5_~(J1$M%|m$uOXt{y z*LgaTB|l1>xrZ+&<8Wk?nieN;gW2@ET_s)_9PYwoNnqHco{9 zLbvQ914tKf9cZT}Z84Y4s#!70KU%`3uJFbtEE?WE_G(5q&#cN++p-SYoEJ8W{GFvU zw;(DRAu(-&G))*p7YT!qN6|1(#shrnB?;exEToCyO>}b=I#Reo$c4JpiMnZBYF%yB z7b}m}vY5;GN9*sRm0Rclkgq&{U3Pp?Md`&&_yt=~OSGV({-Po^aZuN1W}l;|g6*6O zWR&Luh`<7Xax;&o;)NAc(D?bMQPrq?iWSo^#$3%teVde5(JdT~FPSq!V+=ep8@n{m z!hdj9m$38nR~V*iZ1Oqei@JC`^{K8c3_x5I-3$4eki+2mg}jOP2;9q%Aw1>5cg*n; zXoY10!2s6+*z)H`=%*?cWG zODEpPjzw&F7TrBfbhcb5!K|BPMtHHjMS55Djfv_781TjQ0m=tlUWY zuCM8_qh&|aN`W<6Hne2ZC1ub2C96A~J##8Ap~;m`Q@l&47%!+H9uV`J6MhY8xPxvv zvLs!a6=TO0(@c8NbWkMDFXwqs&#(mk`!ZgjWFnUNt;N^|KDKyP&S-)NBaX;Djl~GR zy|}6AH{k`TfM7Y^{)@{s(7Y{OEx(;7ad?~dX_y5hAKk4txn!`Tjvh2o&$@|0x!!*AG;WLmK_qJJa6;REhK(;q5g z*3GS;t)RZciFeU?C?|#WMdJg?Z?jXiyBzV5IdVGk<>Ng}e1ulgiMpv*S^@sm(Hd=i zaWHH?1L22JNOCA)=0B74P7c!j4WAWC^%Jk6h2Kf~O2Lxw6-dKZ*`Y~D<=z+lq$ Copyright 2007, Manuel Jacob + + +import re + +# the following two functions are from the 'original' pypsyc +# (and slightly modified) + +def get_host(uni): + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/(.+)?\/\@(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/(.+)$", uni) + if m: return m.group(1) + + raise "invalid uni" + +def get_user(uni): + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(1) + + raise "invalid uni" + +def psyctext(packet): + text = packet.data + for key, value in packet.vars.items(): + text = text.replace('[%s]' % key, str(value)) + return text + + +class Vars(dict): + def __init__(self, vars, existing = {}): + dict.__init__(self, existing) + self.vars = vars + self.vars.update(existing) + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self.vars.__setitem__(key, value) + + +all_mmpvars = ( +'_source', '_source_identification', '_source_location', '_source_relay', +'_target', '_context', '_counter', '_length', '_initialize', '_fragment', +'_encoding', '_amount_fragments', '_list_using_modules', +'_list_require_modules', '_list_understand_modules', '_list_using_encoding', +'_list_require_encoding', '_list_understand_encoding', +'_list_using_protocols', '_list_require_protocols', +'_list_understand_protocols', '_trace', '_tag', '_tag_relay', '_relay') diff --git a/mjacob/lib/packets.py b/mjacob/lib/packets.py new file mode 100755 index 0000000..c9e19b7 --- /dev/null +++ b/mjacob/lib/packets.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## packets.py + + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +from __init__ import Vars + +class MMPPacket: + def __init__(self, mmpvars = {}, body = ''): + self.vars = {} + self.mmpvars = Vars(self.vars, mmpvars) + self.body = body + + +class PSYCPacket(MMPPacket): + def __init__(self, mmpvars = {}, psycvars = {}, mc = None, data = ''): + MMPPacket.__init__(self, mmpvars) + self.psycvars = Vars(self.vars, psycvars) + self.mc = mc + self.data = data diff --git a/mjacob/lib/parse.py b/mjacob/lib/parse.py new file mode 100755 index 0000000..568b6d6 --- /dev/null +++ b/mjacob/lib/parse.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## parse.py + + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + + +from packets import MMPPacket, PSYCPacket + +def parse_vars(lines): + cvars = {} + pvars = {} + + _list = None + + for i in lines: + tmp = i.split('\t') + if tmp[0] == ':': + vars[_list].append(tmp[1]) + continue + + elif tmp[0].startswith(':'): + vars = cvars + + elif tmp[0].startswith('='): + vars = pvars + + if tmp[0][1:6] == '_list': + _list = tmp[0][1:] + vars[_list] = [tmp[1]] + + else: + _list = None + vars[tmp[0][1:]] = tmp[1] + + return cvars, pvars + + +class LineBased: + def __init__(self): + self.__buffer = '' + self.linemode = True + + def data(self, data): + self.__buffer += data + while self.linemode and self.__buffer: + try: + line, self.__buffer = self.__buffer.split('\n', 1) + self.line(line) + except ValueError: + return + + else: + if not self.linemode: + __buffer = self.__buffer + self.__buffer = '' + self.raw(__buffer) + + +class MMPParser(LineBased): # callback-based + pvars = {} + def __init__(self): + LineBased.__init__(self) + self.reset() + + def reset(self): + self.mode = 'vars' + self.varlines = [] + + self.cvars = {} + self.body = '' + + def line(self, line): + if line == '.': + self.recv() + elif self.mode == 'vars': + if line: + self.varlines.append(line) + else: + self.cvars, pvars = parse_vars(self.varlines) + + for key, value in pvars.items(): + if value: + self.pvars[key] = value + else: + if key in self.pvars: + del self.pvars[key] + + self.mode = 'body' + + else: + self.body += line + '\n' + + def recv(self): + vars = self.pvars.copy() + vars.update(self.cvars) + if self.body and self.body[-1] == '\n': + self.body = self.body[:-1] + packet = MMPPacket(vars, self.body) + packet.not_to_render = True + self.recv_packet(packet) + self.reset() + + def recv_packet(self, packet): + raise NotImplementedError + + +class PSYCParser: + pvars = {} + + def parse(self, packet): + lines = packet.body.split('\n') + _is_text = False + cvars = {} + varlines = [] + textlines = [] + mc = '' + + for i in lines: + if not i: + continue + + tmp = i.split('\t') + + if _is_text: + textlines.append(i) + + elif i[0] in (':', '='): + varlines.append(i) + + elif i[0] == '_': + cvars, pvars = parse_vars(varlines) + + for key, value in pvars.items(): + if value: + self.pvars[key] = value + else: + if key in self.pvars: + del self.pvars[key] + + mc = i + _is_text = True + + text = '\n'.join(textlines) + vars = self.pvars.copy() + vars.update(cvars) + packet = PSYCPacket(packet.mmpvars, vars, mc, text) + packet.not_to_render = True + return packet + + +if __name__ == '__main__': + MMPParser().data(':_context\ti\n=_foo\tbar\n\n_message_public\nHello\n.\n') + MMPParser().data(':_context\ti\n\n_message_public\nHello\n.\n') diff --git a/mjacob/lib/render.py b/mjacob/lib/render.py new file mode 100755 index 0000000..1df188d --- /dev/null +++ b/mjacob/lib/render.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## render.py + + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +from packets import MMPPacket + +def to_vars(cvars = {}, pvars = {}, other = {}): + data = '' + for key, value in cvars.items(): + data += ":%s\t%s\n" % (key, value) + for key, value in pvars.items(): + data += "=%s\t%s\n" % (key, value) + for key, value in other.items(): + data += "%s\t%s\n" % (key, value) + return data + + +class MMPRenderer: + pvars = {} + new_pvars = {} + def render(self, packet): + data = '' + if packet.mmpvars: + data += to_vars(packet.mmpvars, self.new_pvars) + '\n' + self.new_pvars = {} + + data += '%s\n.\n' % packet.body + + return data.encode('utf-8') + + def set_pvar(self, key, value): + self.new_pvars[key] = value + self.pvars[key] = value + + def del_pvar(self, key): + self.new_pvars[key] = '' + del self.pvars[key] + + +class PSYCRenderer: + pvars = {} + new_pvars = {} + def render(self, packet): + data = '' + if packet.psycvars: + data += to_vars(packet.psycvars, self.new_pvars) + self.new_pvars = {} + + if packet.mc: + data += packet.mc + '\n' + + if packet.data: + data += packet.data + + return MMPPacket(packet.mmpvars, data) + + def set_pvar(self, key, value): + self.new_pvars[key] = value + self.pvars[key] = value + + def del_pvar(self, key): + self.new_pvars[key] = '' + del self.pvars[key] diff --git a/mjacob/old/MMP/MMPProtocol.py b/mjacob/old/MMP/MMPProtocol.py new file mode 100755 index 0000000..70e75d4 --- /dev/null +++ b/mjacob/old/MMP/MMPProtocol.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +from twisted.protocols import basic + + +def convert_lines(lines): + """convert list of lines to vars, method (mc) and text""" + _is_text = False + dict = {} + mc = None + textlines = [] + for i in lines: + tmp = i.split('\t') + + if _is_text: + textlines.append(i) + + elif tmp[0] == ':': # second or later item in the list + dict[_list].append(tmp[1]) + + elif i[0] in (':', '='): + if tmp[0][1:6] == '_list': + _list = tmp[0][1:] + dict[_list] = [tmp[1]] + else: dict[tmp[0][1:]] = tmp[1] + + elif i[0] == '_': + mc = i + _is_text = True + + text = '\n'.join(textlines) + return dict, mc, text + + +class MMPProtocol(basic.LineReceiver): + def __init__(self, callback): + self.msg = callback + self.delimiter = '\n' + self.initialized = False + self.charset = 'UTF-8' + self.reset() + + def reset(self): + self.lines = [] + self._reset() + + def _reset(self): + raise NotImplementedError('Implement in Subclass.') + + def lineReceived(self, line): + if not self.initialized: + assert line == '.', 'first line is not ".", line is "%s"' % line + self.initialized = True + return + + if line == '.': # packet ends + self.recv_packet() + else: + self.lines.append(line) + + def recv_packet(self): + h2b = self.lines.index('') # head to body delimiter + mmp = self.lines[:h2b] + data = self.lines[h2b+1:] + mmpvars = convert_lines(mmp)[0] + if '_length' in mmpvars: + bodylength = mmpvars['_length'] + else: + bodylength = len(data) + + if len(data) == bodylength: + self._recv_packet(mmpvars, data) + self.reset() + + def _recv_packet(self, mmpvars, data): + raise NotImplementedError('Implement in Subclass.') + + def _send_packet(self, mmpvars, data): + mmpstring = '' + if mmpvars: + for key, value in mmpvars.items(): + mmpstring += "=%s\t%s\n" % (key, value) + + if not data: + data = '' + + send = "%s%s.\n" % (mmpstring, data) + #print ('send', send.encode(self.charset)) + self.transport.write(send.encode(self.charset)) diff --git a/mjacob/old/MMP/__init__.py b/mjacob/old/MMP/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/old/PSYC/PSYCPacket.py b/mjacob/old/PSYC/PSYCPacket.py new file mode 100755 index 0000000..f8202df --- /dev/null +++ b/mjacob/old/PSYC/PSYCPacket.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +class PSYCPacket: + def __init__(self, mmpvars = {}, psycvars = {}, mc = None, text = ''): + self.mmpvars = mmpvars + self.psycvars = psycvars + self.vars = mmpvars.copy() + self.vars.update(psycvars) + self.mc = mc + self.text = text + + def __repr__(self): + return 'vars: %s, mc: %s, text: %s' % (self.vars, self.mc, self.text) diff --git a/mjacob/old/PSYC/PSYCProtocol.py b/mjacob/old/PSYC/PSYCProtocol.py new file mode 100755 index 0000000..7f2858d --- /dev/null +++ b/mjacob/old/PSYC/PSYCProtocol.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + + +from MMP.MMPProtocol import MMPProtocol, convert_lines +from PSYC.PSYCPacket import PSYCPacket + +class PSYCProtocol(MMPProtocol): + mmpvars = ( +'_source', '_source_identification', '_source_location', '_source_relay', +'_target', '_context', '_counter', '_length', '_initialize', '_fragment', +'_encoding', '_amount_fragments', '_list_using_modules', +'_list_require_modules', '_list_understand_modules', '_list_using_encoding', +'_list_require_encoding', '_list_understand_encoding', +'_list_using_protocols', '_list_require_protocols', +'_list_understand_protocols', '_trace', '_tag', '_tag_relay', '_relay') + + def _reset(self): + pass + + def __init__(self, callback): + MMPProtocol.__init__(self, callback) + + def _recv_packet(self, mmpvars, data): + #print 'recv' + psycvars, mc, text = convert_lines(data) + packet = PSYCPacket(mmpvars, psycvars, mc, text) + + func = getattr(self, "handle_%s" % packet.mc[1:], None) + + not_call_msg = False + + if func: + not_call_msg = func(packet) + + if not not_call_msg: + self.msg(packet) + + def handle_status_circuit(self, packet): + self._send_packet(None, None) # send an empty mmp packet (single dot) + self.send_packet(PSYCPacket(mmpvars = + {'_target': self.factory.config.uni}, mc = '_request_link')) + + self.charset = packet.vars['_using_characters'] + + def send_packet(self, packet): + self.factory.ui.outpacketcount += 1 + self.factory.ui.pre_send(packet) + data = '' + + if packet.psycvars: + data += '\n' + for key, value in packet.psycvars.items(): + data += ":%s\t%s\n" % (key, value) + + if packet.mc: + data += packet.mc + '\n' + + if packet.text: + data += packet.text + '\n' + + self._send_packet(packet.mmpvars, data) + + ################################ + # implementation of the PSYC API + ################################ + + def msg(self, packet): + raise NotImplementedError + + def sendmsg(self, target, mc, data, vars): + mmpvars = {'_target': target} + psycvars = {} + for key, value in vars.items(): + if i in self.mmpvars: + mmpvars[key] = value + else: + psycvars[key] = value + + packet = PSYCPacket(mmpvars, psycvars, mc, data) + self.send_packet(packet) + + def castmsg(self, source, mc, data, vars): + mmpvars = {'_context': source} + psycvars = {} + for key, value in vars.items(): + if i in self.mmpvars: + mmpvars[key] = value + else: + psycvars[key] = value + + packet = PSYCPacket(mmpvars, psycvars, mc, data) + self.send_packet(packet) diff --git a/mjacob/old/PSYC/__init__.py b/mjacob/old/PSYC/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/old/UI/__init__.py b/mjacob/old/UI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/old/UI/base.py b/mjacob/old/UI/base.py new file mode 100755 index 0000000..faa361f --- /dev/null +++ b/mjacob/old/UI/base.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +import re + +from twisted.internet import reactor + +from PSYC.PSYCPacket import PSYCPacket + +re_var = re.compile('\[_.[^\[]+\]') + +class BaseUI: + ignored_mcs = [] + ignored_users = [] + mc_prefixes = ('_notice', '_error', '_info', '_status') + inpacketcount = 0 + outpacketcount = 0 + connected = False + + def recv(self, packet): + self.inpacketcount += 1 + if packet.mc in self.ignored_mcs: + return + + #if packet.vars['_source'] in self.ignored_users: + # return + + self.pre_print(packet) + self._insert_vars(packet) + self.print_(packet) + + def _insert_vars(self, packet): + while True: + x = re_var.search(packet.text) + if not x: + break + tmp = x.group() + if tmp[1:-1] in packet.vars: + packet.text = packet.text.replace(tmp, packet.vars[tmp[1:-1]]) + + def print_(self, packet): + func = None + for i in self.mc_prefixes: + if packet.mc.startswith(i): + func = getattr(self, 'handle_%s_' % i[1:], None) + + tmp = getattr(self, 'handle_%s' % packet.mc[1:], None) + + if tmp: + func = tmp + + if func: + func(packet) + + else: + self._print('Server', 'unhandled packetmc (Packet %s)' + % self.inpacketcount) + + def handle_notice_(self, packet): + self._print('Server', "%s: %s" % (packet.mc, packet.text)) + + def handle_error_(self, packet): + self._print('Server', "%s: %s" % (packet.mc, packet.text)) + + def handle_info_(self, packet): + self._print('Server', "%s: %s" % (packet.mc, packet.text)) + + def handle_status_(self, packet): + self._print('Server', "%s: %s" % (packet.mc, packet.text)) + + def handle_message_public(self, packet): + self._print(packet.vars['_context'], '%s says: %s' + % (packet.vars['_nick'], packet.text)) + + def handle_status_place_topic_official(self, packet): + pass + + def handle_echo_logoff(self, packet): + reactor.stop() + + def server_command(self, command, target): + if target == 'Server': + target = 'psyc://%s/' % self.factory.config.host + + if command.startswith('/go'): + self.factory.sc.send_packet(PSYCPacket(mmpvars = {'_target': command[4:]}, psycvars = {'_nick': self.factory.config.username}, mc = '_request_enter')) + + elif command.startswith('/join'): + self.factory.sc.send_packet(PSYCPacket(mmpvars = {'_target': command[6:]}, psycvars = {'_nick': self.factory.config.username}, mc = '_request_enter')) + + elif command.startswith('/quit'): + self.factory.sc.send_packet(PSYCPacket(mmpvars = {'_target': self.factory.config.uni, '_source_identification': self.factory.config.uni}, mc = '_request_execute', text = 'bye')) + + + else: + self.factory.sc.send_packet(PSYCPacket(mmpvars = {'_target': self.factory.config.uni, '_source_identification': self.factory.config.uni}, mc = '_request_input', text = command)) + + #if command.startswith('/quit'): + # reactor.stop() + + #if command.startwith('/part'): + # self.factory.sc.send_packet(PSYCPacket()) diff --git a/mjacob/old/UI/text/Interface.py b/mjacob/old/UI/text/Interface.py new file mode 100755 index 0000000..aef78c2 --- /dev/null +++ b/mjacob/old/UI/text/Interface.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +from UI import base + +class Interface(base.BaseUI): + def __init__(self, factory): + self.factory = factory + self.factory.connect() + #BaseUI.__init__(self) + + def _print(self, window, text): + print '%s: %s' % (window, text) diff --git a/mjacob/old/UI/text/__init__.py b/mjacob/old/UI/text/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/old/UI/wx/Interface.py b/mjacob/old/UI/wx/Interface.py new file mode 100755 index 0000000..b49d88b --- /dev/null +++ b/mjacob/old/UI/wx/Interface.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +import wx + +from twisted.internet import reactor + +from UI import base +from PSYC.PSYCPacket import PSYCPacket + +class Tab(wx.Panel): + def __init__(self, parent, name): + self.name = name + wx.Panel.__init__(self, parent, -1) + self.textctrl = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE|wx.TE_READONLY) + self.users = [] + + def OnResize(event): + self.textctrl.SetSize(event.GetSize()) + self.Bind(wx.EVT_SIZE, OnResize, self) + + +class MainWindow(wx.Frame): + def __init__(self, ui): + wx.Frame.__init__(self, None, -1, size = (800, 600)) + + self.Bind(wx.EVT_CLOSE, self.OnClose, self) + + self.ui = ui + self.factory = self.ui.factory + + self.tabs = {} + splitter = wx.SplitterWindow(self, -1) + + self.pl = wx.Panel(splitter, -1) + sizer = wx.GridBagSizer(0, 0) + + self.nb = wx.Notebook(self.pl, -1, style = wx.NB_TOP) + sizer.Add(self.nb, (0, 0), (1, 1), wx.EXPAND) + sizer.AddGrowableCol(0) + sizer.AddGrowableRow(0) + + self.input = wx.TextCtrl(self.pl, -1) + sizer.Add(self.input, (1, 0), (1, 1), wx.EXPAND) + self.pl.SetSizerAndFit(sizer) + self.add_tab('Server') + + self.pr = wx.Panel(splitter, -1) # right panel + self.userlist = wx.ListBox(self.pr, -1, (0, 5), style = wx.SUNKEN_BORDER) + + self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnChangeTab, self.nb) + + def OnResize(event): + size = event.GetSize() + self.userlist.SetSize((size[0], size[1] - 10)) + self.pr.Bind(wx.EVT_SIZE, OnResize, self.pr) + + splitter.SplitVertically(self.pl, self.pr, self.GetSize()[0] * 0.7) + splitter.SetSashGravity(1) + + mainmenu = wx.Menu() + + menuitem = mainmenu.Append(-1, '&Connect', 'Connect to the server') + self.Bind(wx.EVT_MENU, self.OnConnect, menuitem) + + menuitem = mainmenu.Append(-1, '&Debug window', + 'Open or close debug window') + self.Bind(wx.EVT_MENU, self.OnOpenDebugWindow, menuitem) + + mainmenu.AppendSeparator() + + menuitem = mainmenu.Append(-1, '&Exit', 'Exit pyPSYC') + self.Bind(wx.EVT_MENU, self.OnClose, menuitem) + + menubar = wx.MenuBar() + menubar.Append(mainmenu, '&pyPSYC') + + self.SetMenuBar(menubar) + + self.input.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + + self.Show(True) + + def OnKeyDown(self, event): + if event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): + pagename = self.nb.GetCurrentPage().name + value = self.input.GetValue() + if value: + if pagename == 'Server' or value.startswith('/'): + self.ui.server_command(value, self.nb.GetCurrentPage().name) + + elif '@' in pagename: + self.factory.sc.castmsg(pagename, '_message_public', + value, {}) + + elif '~' in pagename: + self.factory.sc.sendmsg(pagename, '_message_private', + value, {}) + + self.input.SetValue('') + + event.Skip() + + def OnChangeTab(self, event): + self.userlist.Set(self.nb.GetCurrentPage().users) + + def OnConnect(self, event): + self.factory.connect() + self.ui.connected = True + + def OnOpenDebugWindow(self, event): + # open or close window: + if not self.ui.debugwindow.Show(True): self.ui.debugwindow.Show(False) + + def OnClose(self, event): + if self.ui.connected: + self.ui.server_command('/quit', None) + else: + reactor.stop() + + def add_tab(self, name): + self.tabs[name] = Tab(self.nb, name) + self.nb.AddPage(self.tabs[name], name) + + +class DebugWindow(wx.Frame): + def __init__(self, factory): + wx.Frame.__init__(self, parent = None, id = -1, + title = 'pyPSYC debug window', size = (800, 600), + style = wx.DEFAULT_FRAME_STYLE| + wx.NO_FULL_REPAINT_ON_RESIZE) + self.Bind(wx.EVT_CLOSE, self.OnClose, self) + + self.tree = wx.TreeCtrl(self, -1, wx.DefaultPosition, (-1,-1), + wx.TR_HIDE_ROOT|wx.TR_HAS_BUTTONS) + self.treeroot = self.tree.AddRoot('Programmer') + + def OnClose(self, event): + self.Show(False) + + +class Interface(base.BaseUI): + def __init__(self, factory): + self.factory = factory + self.gui = wx.PySimpleApp() + self.mainwindow = MainWindow(self) + self.debugwindow = DebugWindow(self) + + def _print(self, window, text): + if window not in self.mainwindow.tabs: + self.mainwindow.add_tab(window) + self.mainwindow.tabs[window].textctrl.AppendText(text + '\n') + + def pre_print(self, packet): + print ('recv', packet) + tmp = self.debugwindow.tree.AppendItem(self.debugwindow.treeroot, + 'Incoming Packet %s' + % self.inpacketcount) + self.debugwindow.tree.AppendItem(tmp, "MMPVars: %s" % packet.mmpvars) + self.debugwindow.tree.AppendItem(tmp, "PSYCVars: %s" % packet.psycvars) + self.debugwindow.tree.AppendItem(tmp, "Method: %s" % packet.mc) + self.debugwindow.tree.AppendItem(tmp, "Text: %s" % packet.text) + + def pre_send(self, packet): + print ('send', packet) + tmp = self.debugwindow.tree.AppendItem(self.debugwindow.treeroot, + 'Outgoing Packet %s' + % self.outpacketcount) + self.debugwindow.tree.AppendItem(tmp, "MMPVars: %s" % packet.mmpvars) + self.debugwindow.tree.AppendItem(tmp, "PSYCVars: %s" % packet.psycvars) + self.debugwindow.tree.AppendItem(tmp, "Method: %s" % packet.mc) + self.debugwindow.tree.AppendItem(tmp, "Text: %s" % packet.text) + + def handle_echo_place_enter_login(self, packet): + self._print(packet.vars['_source_relay'], packet.text) + self.mainwindow.tabs[packet.vars['_source_relay']].users = packet.vars['_list_members'] + + def handle_echo_place_enter(self, packet): + self._print(packet.vars['_nick_place'], packet.text) + self.mainwindow.tabs[packet.vars['_nick_place']].users = packet.vars['_list_members'] + + +if __name__ == '__main__': + app = wx.PySimpleApp() + MainWindow() + app.MainLoop() + +##class Tab(wx.Panel): +## def __init__(self, name, main): +## self.name = name +## self.main = main +## self.ui = self.main.ui +## self.factory = self.ui.factory +## wx.Panel.__init__(self, main.nb, -1) +## self.textbox = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE| +## wx.TE_READONLY,) +## self.input = wx.TextCtrl(self, -1) +## #self.input.SetFocus() +## self.input.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) +## +## def OnKeyDown(self, event): +## if event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): +## if self.input.GetValue(): +## self.factory.sc.castmsg(self.name, '_message_public', +## self.input.GetValue(), {}) +## self.input.SetValue('') +## +## event.Skip() +# +# +#class Tab(wx.Panel): +# def __init__(self, name, main): +# self.name = name +# self.main = main +# self.ui = self.main.ui +# if hasattr(self.ui, 'factory'): +# self.factory = self.ui.factory +# wx.Panel.__init__(self, main.nb, -1, size = wx.DefaultSize) +# #wx.Frame.__init__(self, None, -1, size = wx.DefaultSize) +# +# #splitter = wx.SplitterWindow(self, -1) +# #pl = wx.Panel(splitter, -1, size = wx.DefaultSize) # panel left +# ##pls = wx.BoxSizer(wx.VERTICAL) +# #wx.TextCtrl(pl, -1, style = wx.TE_MULTILINE|wx.TE_READONLY, size = wx.DefaultSize) +# +# #pr = wx.Panel(splitter, -1, size = wx.DefaultSize) # panel right +# #wx.ListCtrl(pr, -1, size = wx.DefaultSize) +# +# #splitter.SplitVertically(pl, pr) +# +# splitter = wx.SplitterWindow(self, -1) +# panel1 = wx.Panel(splitter, -1) +# wx.StaticText(panel1, -1, +# "Whether you think that you can, or that you can't, you are usually right." +# "\n\n Henry Ford", +# (100,100), style=wx.ALIGN_CENTRE) +# panel1.SetBackgroundColour(wx.LIGHT_GREY) +# panel2 = wx.Panel(splitter, -1) +# panel2.SetBackgroundColour(wx.WHITE) +# splitter.SplitVertically(panel1, panel2) +# self.Centre() +# +# +#class MainWindow(wx.Frame): +# def __init__(self, ui): +# wx.Frame.__init__(self, parent = None, id = -1, title = 'pyPSYC', +# size = (800, 600), +# style = wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE) +# +# self.Bind(wx.EVT_CLOSE, self.OnClose, self) +# +# self.ui = ui +# if hasattr(self.ui, 'factory'): +# self.factory = self.ui.factory +# +# self.CreateStatusBar() +# +# # Set up and create the menu +# mainmenu = wx.Menu() +# +# menuitem = mainmenu.Append(-1, '&Connect', 'Connect to the server') +# self.Bind(wx.EVT_MENU, self.OnConnect, menuitem) +# +# menuitem = mainmenu.Append(-1, '&Debug window', +# 'Open or close debug window') +# self.Bind(wx.EVT_MENU, self.OnOpenDebugWindow, menuitem) +# +# mainmenu.AppendSeparator() +# +# menuitem = mainmenu.Append(-1, '&Exit', 'Exit pyPSYC') +# self.Bind(wx.EVT_MENU, self.OnClose, menuitem) +# +# menubar = wx.MenuBar() +# menubar.Append(mainmenu, '&pyPSYC') +# self.SetMenuBar(menubar) +# +# self.nb = wx.Notebook(self, -1, style=wx.NB_BOTTOM) +# +# self.tabs = {} +# +# self.Show(True) +# +# def OnConnect(self, event): +# self.factory.connect() +# +# def OnOpenDebugWindow(self, event): +# # open or close window: +# if not self.ui.debugwindow.Show(True): self.ui.debugwindow.Show(False) +# +# def OnClose(self, event): +# reactor.stop() +# +# +#class DebugWindow(wx.Frame): +# def __init__(self, factory): +# wx.Frame.__init__(self, parent = None, id = -1, +# title = 'pyPSYC debug window', size = (800, 600), +# style = wx.DEFAULT_FRAME_STYLE| +# wx.NO_FULL_REPAINT_ON_RESIZE) +# self.Bind(wx.EVT_CLOSE, self.OnClose, self) +# +# self.tree = wx.TreeCtrl(self, -1, wx.DefaultPosition, (-1,-1), +# wx.TR_HIDE_ROOT|wx.TR_HAS_BUTTONS) +# self.treeroot = self.tree.AddRoot('Programmer') +# +# def OnClose(self, event): +# self.Show(False) +# +# +#class Interface:#(base.BaseUI): +# def __init__(self, factory): +# self.gui = wx.PySimpleApp() +# if factory: +# self.factory = factory +# reactor.registerWxApp(self.gui) +# self.mainwindow = MainWindow(self) +# self.debugwindow = DebugWindow(self) +# window = 'Server' +# self.mainwindow.tabs[window] = Tab(window, self.mainwindow) +# self.mainwindow.nb.AddPage(self.mainwindow.tabs[window], window) +# +# def _print(self, window, text): +# if window not in self.mainwindow.tabs: +# self.mainwindow.tabs[window] = Tab(window, self.mainwindow) +# +# #def OnOvrSize(event, tab=self.mainwindow.tabs[window]): +# # size = event.GetSize() +# # textboxsize = (size[0], size[1] - 24) +# # inputpos = (0, size[1] - 24) +# # tab.textbox.SetSize(textboxsize) +# # tab.input.SetPosition(inputpos) +# # tab.input.SetSize((size[0], 21)) +# +# #wx.EVT_SIZE(self.mainwindow.tabs[window], OnOvrSize) +# +# self.mainwindow.nb.AddPage(self.mainwindow.tabs[window], window) +# +# self.mainwindow.tabs[window].textbox.AppendText(text) +# +# +# def pre_print(self, packet): +# tmp = self.debugwindow.tree.AppendItem(self.debugwindow.treeroot, +# 'Incoming Packet %s' +# % self.inpacketcount) +# self.debugwindow.tree.AppendItem(tmp, "MMPVars: %s" % packet.mmpvars) +# self.debugwindow.tree.AppendItem(tmp, "PSYCVars: %s" % packet.psycvars) +# self.debugwindow.tree.AppendItem(tmp, "Method: %s" % packet.mc) +# self.debugwindow.tree.AppendItem(tmp, "Text: %s" % packet.text) +# +# def pre_send(self, packet): +# tmp = self.debugwindow.tree.AppendItem(self.debugwindow.treeroot, +# 'Outgoing Packet %s' +# % self.outpacketcount) +# self.debugwindow.tree.AppendItem(tmp, "MMPVars: %s" % packet.mmpvars) +# self.debugwindow.tree.AppendItem(tmp, "PSYCVars: %s" % packet.psycvars) +# self.debugwindow.tree.AppendItem(tmp, "Method: %s" % packet.mc) +# self.debugwindow.tree.AppendItem(tmp, "Text: %s" % packet.text) +# +#if __name__ == '__main__': +# interface = Interface(None) +# interface.gui.MainLoop() diff --git a/mjacob/old/UI/wx/__init__.py b/mjacob/old/UI/wx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/old/__init__.py b/mjacob/old/__init__.py new file mode 100755 index 0000000..f4c8100 --- /dev/null +++ b/mjacob/old/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +import re + +# the following two functions are from the 'original' pypsyc +# (and slightly modified) + +def get_host(uni): + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/(.+)?\/\@(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/(.+)$", uni) + if m: return m.group(1) + + raise "invalid uni" + +def get_user(uni): + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(1) + + raise "invalid uni" diff --git a/mjacob/old/pypsyc-twisted.py b/mjacob/old/pypsyc-twisted.py new file mode 100755 index 0000000..e08dbe3 --- /dev/null +++ b/mjacob/old/pypsyc-twisted.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the GPLv2 license +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + +# Copyright 2007, Manuel Jacob + +import os + +from __init__ import get_host, get_user +from PSYC.PSYCProtocol import PSYCProtocol +from PSYC.PSYCPacket import PSYCPacket + +try: + from twisted.internet import wxreactor + wxreactor.install() +except ImportError: + pass + +from twisted.internet import reactor, protocol + +class Config: + def __init__(self, factory): + self.factory = factory + self.settings_folder = self.get_settings_folder() + + self.uni_file = os.path.join(self.settings_folder, 'me') + self.clientconfig_file = os.path.join(self.settings_folder, 'pypsyc', 'config') + + self.uni = self.get_uni() + #self.uni = 'psyc://beta.ve.symlynX.com/~pypsyctest' + #self.uni = raw_input('uni?: ') + self.host = get_host(self.uni) + self.username = get_user(self.uni) + + self.configfile = self.open_configfile() + + self.ui = 'wx' + + def get_settings_folder(self): + # resolve settings folder + if os.name == 'posix': + folder = os.path.join(os.environ.get('HOME'), ".psyc") + elif os.name == 'nt': + folder = os.path.join(os.environ.get('APPDATA'), 'psyc') + else: + folder = os.path.join(os.getcwd(), 'psyc') + + # create settings folder if necessary + try: + os.mkdir(folder) + except OSError: + pass + + return folder + + def get_uni(self): + f = file(self.uni_file) + uni = f.read().strip() + f.close() + return uni + + def open_configfile(self): + return None + + +class App(protocol.ClientFactory): + def __init__(self): + self.config = Config(self) + self.protocol = PSYCProtocol + self.connections = {} + + #Interface = __import__('UI.%s.Interface' % self.config.ui, [], [], ['Interface']) + exec("from UI.%s.Interface import Interface" % self.config.ui) + self.ui = Interface(self) + + self.callback_on_recv = self.ui.recv + + def connect(self): + reactor.connectTCP(self.config.host, 4404, self, timeout = 20) + + def buildProtocol(self, addr): + p = self.protocol(self.callback_on_recv) + p.factory = self + self.callback_on_recv + self.connections[addr.host] = p + if len(self.connections) == 1: + self.sc = p # sc = server connection + return p + + +factory = App() +reactor.run() diff --git a/mjacob/old/pypsyc.conf b/mjacob/old/pypsyc.conf new file mode 100644 index 0000000..d205a09 --- /dev/null +++ b/mjacob/old/pypsyc.conf @@ -0,0 +1,4 @@ +[server1] +host = beta.ve.symlynX.com +port = 4404 +username = l diff --git a/mjacob/server.py b/mjacob/server.py new file mode 100755 index 0000000..e5fca0f --- /dev/null +++ b/mjacob/server.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2007, Manuel Jacob + +import os + +from base import Config, BaseCenter, MMPCircuit + + +### TODO: This don't works yet ### + + +class ToClientCircuit(MMPCircuit): + def __init__(self, center): + MMPCircuit.__init__(self, center) + + def init(self): + MMPCircuit.init(self) + #self. + + +class ServerConfig(Config): + def __init__(self, app): + Config.__init__(self, app) + self.serverconfig_file = os.path.join(self.settings_folder, 'config') + + def get_settings_folder(self): + # resolve settings folder + if os.name == 'posix': + folder = os.path.join(os.environ.get('HOME'), ".pypsyced") + elif os.name == 'nt': + folder = os.path.join(os.environ.get('APPDATA'), 'pypsyced') + else: + folder = os.path.join(os.getcwd(), 'pypsyced') + + # create settings folder if necessary + try: + os.mkdir(folder) + except OSError: + pass + + return folder + + def open_configfile(self): + return None + + +class ServerCenter(BaseCenter): + def __init__(self): + self.config = ServerConfig(self) + + def recv_mmp_packet(self, packet, physsource): + if not packet.vars and not packet.body: + return + + if physsource.linked_uni == None or packet.mmpvars['_target']: + psyc_packet = physsource.psyc_parser.parse(packet) + self.recv_psyc_packet(psyc_packet, physsource) diff --git a/mjacob/server.pyc b/mjacob/server.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ca05c850f2f3f3ebf5b053848fddc9616877293 GIT binary patch literal 2309 zcma)7-EJF26h5=--?R-$1CiRI0;*D3LSQaH;to<1!A;z-setBYIC*b?eIJOfJgrymunKS4AJ7=eVKU(|!uf6XRng2rke~VS0VoOENMUFri zIX5y1`?;Gm&V_z#y!gc2R9#bFZQ5f|+bu;JUYAOiX!KDsnU342EFDFP zTGLvN+^G_0MV2cxYwKK=pVaZVn54FHngR_vQT_2ao7fdpFe%IY`P9+CGb~M=m&Mr!d2rxgqkYUx)J*Dj^;Mo z9Zl_Z&NkItNBac`{5ju}hU?1NniOp@_~q!LU(ij5y*8X4I>t~Ib@rPk?uXa=do^^j2mhTkI$qes|O0kIja^NN!}rEGqgRYk4^J3&2I&3QRU z;wt$yRX?WZX;!Js>%+aODyo&raE9EejN9_!@trDvglnB15^R_q6l^DW7KUcitea-A zXj{Tui%0mT%ek^>(qWO?d;3$-Rg^Z5J&=f_FKCyz5Yy8hX3ioe%fps4Ci)oLG7hCY z%$Rissk0Xy+URbs_NdOU1bqE1dh~ydR=(z;ZPK6v=$vxhKf#B}$dlrw z=e8xgf%LmjXU7A%0)d7CoT)?IXM97&PwYk&fYoC^E8xmeat`d#v&&KC$Hj4x&UA{F zr?a{)E~>=pagycdHjOXxEFzRt;dEN6+yh*&G2#+hFi^C8NV*jcO=<4j2A)?oxs2yB zejW@7o>kBFZLYmN-+(Fx-3cvK}GHyr({MaAyMMOEJrUv&pt MH+a(8?ruN*2Mh&Wt^fc4 literal 0 HcmV?d00001 diff --git a/mjacob/socket_/__init__.py b/mjacob/socket_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mjacob/socket_/__init__.pyc b/mjacob/socket_/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6033bb72e1b2ed4b51461b65f8f139b08106fca1 GIT binary patch literal 120 zcmd1(#LHFxz|}pO0SXv_v;zfgc5S_J?G-(sQiUdV`2yyYLxo`(Hf^un9Z4nZ=Sgv;~mN>R$Z;L8{IF!G_ z|Kd0B3z#={>b8Km5NS8NGqawzZ{N)F=Vblsk5{`XwZD-6U-GEG^JIt?M46(wB4rdU z0?I;~hm`eb-eXfhi#}xonh$sr(qfIWA5*0BMs%#m^ywSXInn$1I++34b+REH z2V~YR92;bY;^>KEbm172;c!00Uz><;pbo4 z9n4&no;Y7`@b=*F{jA+Lr^alYWzOb)#=};f_0U$QwrZ??6JgDFc9Pm2w0SO=1A-kM z^)sG4fitFzZ$9Un2C@i=NrI(E`Y`Ho+GETe7Dv;%NEg<}Q~$};-kNy3Jo8AH7I|({ z?}~gcF&2!};Ez;_zts?YciGV3a{+y3l+Z?pGGw>F@!)>BEaNh%YFi0lllVmLArX4O z5glrcqaL{trfq87sWmrw|5q#z*~t)CWV>&GDIgdA0TW1*WDT$pZ+(^Iby-y2_j&iJ za^C&}7~Js!gBfJEGv3gdsLgTx+OOZT>s z;N{9oIYfRC+zyZW5l_F@5~_%sQrJ}y4nRH_GajoVWKpm_zpzkQ9V`t|tn1TxK#Dgj z#=yG<$VMQbvGkBhTNl=dd=q0D9e*flA0bzxW4x8V#Z$+1BWBQibmB6zBJ?pE#o+p3-n=cYxDKrSLfeUd}Hc36p85(OrznvXuG%bM+sb4}2 zyM_R+BgVJd^UrwMR-W{#PEr~@K;OS|Tt3vu86}H{lOka`_xB^1JQ!*l&$dIS&8?Tx zDcfEuNRPO=$~8Lm`)hsMJV+Dz6f02*FGxc46RdDy{g9{Yj6zH=@U+s|C&8wgs7bgr-U_$EU-=a) AHvj+t literal 0 HcmV?d00001 diff --git a/mjacob/twisted_client.py b/mjacob/twisted_client.py new file mode 100755 index 0000000..da1b7a0 --- /dev/null +++ b/mjacob/twisted_client.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from lib import psyctext +from lib.packets import MMPPacket, PSYCPacket +from socket_.twisted_ import TwistedClientCenter#, reactor + +factory = TwistedClientCenter() +#factory.connect(factory.config.host) + +debug = True + +import base +base.debug = debug + +def install_wx(app): # not tested + #del sys.modules['twisted.internet.reactor'] + from twisted.internet import wxreactor + wxreactor.install() + from twisted.internet import reactor + reactor.registerWxApp(app) + +def install_tk(app): # not tested + from twisted.internet import tksupport + tksupport.install(app) + +def install_gtk(app): # not tested + del sys.modules['twisted.internet.reactor'] + from twisted.internet import gtkreactor + gtkreactor.install() + from twisted.internet import reactor + +def install_gtk2(app): # not tested + del sys.modules['twisted.internet.reactor'] + from twisted.internet import gtk2reactor + gtk2reactor.install() + from twisted.internet import reactor + +if debug and __name__ == '__main__': # You can test modules here + from base import Module + + class DebugModule(Module): + methods = ['*'] + def received(self, packet, physsource): + print """packet from %s:: +mmpvars: %s +psycvars: %s +mc: %s +text: %s +""" % (physsource, packet.mmpvars, packet.psycvars, packet.mc, psyctext(packet)) + + factory.register_module(DebugModule()) + + + class AuthModule(Module): + methods = ['_notice_circuit_established'] + + def handle_notice_circuit_established(self, packet, physsource): + physsource.send_psyc_packet(PSYCPacket(mmpvars = {'_target': factory.config.uni}, mc = '_request_link')) + physsource.mmp_renderer.set_pvar('_source_identification', factory.config.uni) + + factory.register_module(AuthModule()) + + +if __name__ == '__main__': + from UI.wx_ import UI + + app = UI(factory) + + from twisted.internet import reactor + factory.reactor = reactor + + print 'start reactor' + reactor.run() diff --git a/mjacob/twisted_client.pyc b/mjacob/twisted_client.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ec8c6f7cdecdd77134d6fe1da6a3ba3f5ed7e0f GIT binary patch literal 2876 zcmcguTW=dh6h6D_OV*9sgj`6usYn!AE!ZN6B1Ir*18VZnsG2sQT0$#lcN}lL-ZeYp zCXrh4(Ed<<1HXXtow1#3`+`KIIhi>-bLPzVor_2RG#h{Y{p;hHrhftd|Cxt*;1MN6 zL!!*1u18+t(a@(Xpl(1}g}N11`82FjR-JYAzVMBO^+1?q;B1T<_=w?R52-Jpmy z6?)4XZctK{$s#2+nKUV>%j70D7e{RC79|UG>U)&jz!ARj zMR%EWozf3TFJt`%zAnyoHRrolX1i8qyK>%ncGDw1c9Vvyl&w*BjlZnn09)&>Q*w)j zA5ykK-3|V*fe+CJB}?>phga!sSoB~n3FujCnel5ed}J0_jLm6m^@(ly(1a|WJbC)G z7Z0^H4PHHc_F~&D-I*2EJwHy3)ya02>fCPg(B)nXivbVV+2&!sV*yr#qSt);E00oN zd;v484(PSdYM;^yy_!Np>>3Q1}TZec~`TNPk@z~<--gqoNs46cM9;Tpf@o>U^$+OOHokYLvj86e! zXLQtyi-XS8k0=&w+T&9T5T{p1$0zVra3{a%HN2)LxB;^{ZlCeu8r;qUguUkgvO_Z& zkBic_D*Qq5AbI~>uW&$KBQ(2rf&H^5DS`65vzB9uorOu7ISLr&j&Ab8^5Zz#J^B%x zN&?QPt~jbW(O6v-suQpagL`HJhcA)Vp3y4N311HF@Ph6N_i)IZ8X>P3x^N9oKEo%Z&(+Xg*`(6z9Fu?sb4|AUdpW?dVDhgyE^9tE2CR`T#Ra$?% zBLN4N_RJQ`vDCFY{jwO{xoftz8l%zpWv?__ylNng=ZjG+I#foPwC3qC9-JCeOv+eW z=$uIxf6ka7Gc#s!`6!<2q}1Xy7HME~)>o@MQ7EgIlMx7)I@ak+oqUDeutlCE&=mf> zH_}lgHHNtm?0E^KM%oUF#3%rLW>{ttAB*{u5_NmMAZ58tW)d z%XpI7NOR#m$Wk-V$yGLY_yAgfM~oHIIv}cWC~zU>u8>wK_<_4tf@0z`t688xg9($evnbDIi3)R~~AsP{^d#N2UIA!tO*_nh)(dt0Tu{lqTHA(taBEY+B^4 z1^y(YNOv(hPa;qnP3cCeopnaJt1m6;RDvQd@_u?Ky=0Q77Rkhs#+T=bE_Es78Eqp3 zpX)TYao;;50oga`UB11$o4TCMN9AWn)HRzg5X=(4w3X-HV!a7S2l$3T<0A92mU`G1U{FjCLg1T2Li|F%R1Y9J5Z^!`3!TG& zE3XTV>sPV<}j#uLez?FY*tI(64&`0_eB{ A(EtDd literal 0 HcmV?d00001 diff --git a/pypsyc/GUI/Gtk/__init__.py b/pypsyc/GUI/Gtk/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/pypsyc/GUI/Gtk/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/pypsyc/GUI/Gtk2/__init__.py b/pypsyc/GUI/Gtk2/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/pypsyc/GUI/Gtk2/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/pypsyc/GUI/Qt/Gui.py b/pypsyc/GUI/Qt/Gui.py new file mode 100644 index 0000000..8bc1700 --- /dev/null +++ b/pypsyc/GUI/Qt/Gui.py @@ -0,0 +1,167 @@ +import sys +import qt +from qt import SIGNAL, SLOT +import asyncore + +import pypsyc.GUI.Abstract.Gui as AbstractGui + +class MainDebugWidget(qt.QWidget): + def __init__(self, parent = None, name = None, fl=0): + qt.QWidget.__init__(self,parent,name,fl) + + self.textField = qt.QTextBrowser(self) + self.textField.setGeometry(10, 5, 580, 350) + + self.inputField = qt.QMultiLineEdit(self) + self.inputField.setGeometry(10, 360, 580 , 100) + + qt.QObject.connect(self.inputField, SIGNAL('returnPressed()'), + self.inputText) + + self.resize(qt.QSize(600,480).expandedTo(self.minimumSizeHint())) + sys.stdout = self + + def inputText(self): + if self.inputField.text().findRev('\n.\n') - self.inputField.text().length() == -3: + data = self.inputField.text() + print "packet entered" + self.inputField.setText('') + def write(self, string): + self.textField.append(string) + + +class RoomWidget(qt.QWidget): + def __init__(self, parent = None, name = None, fl = 0): + qt.QWidget.__init__(self, parent, name, fl) + self.name = name + + self.textField = qt.QTextBrowser(self) + self.textField.setGeometry(qt.QRect(10, 35, 470, 350)) + + self.topicField = qt.QLineEdit(self) + self.topicField.setGeometry(qt.QRect(10, 5, 580, 25)) + + self.nicklist = qt.QListBox(self) + self.nicklist.setGeometry(qt.QRect(490, 35, 100, 350)) + + self.inputField = qt.QLineEdit(self) + self.inputField.setGeometry(qt.QRect(10, 390, 470, 20)) + + qt.QObject.connect(self.inputField, SIGNAL('returnPressed()'), + self.inputText) + qt.QObject.connect(self.topicField, SIGNAL('returnPressed()'), + self.inputTopic) + + self.resize(qt.QSize(600,550).expandedTo(self.minimumSizeHint())) + + def inputText(self): + print self.name + " input: " + self.inputField.text().stripWhiteSpace().latin1() + self.inputField.setText('') + + def inputTopic(self): + print self.name + " topic: " + self.topicField.text().stripWhiteSpace().latin1() + + def append(self, text): self.textField.append(text.strip() + '\n') + def set_topic(self, text): self.topicField.setText(text) + def inputNickCurrent(self): + print self.name + " current nick:" + self.nickField.text().stripWhiteSpace().latin1() + + +from pypsyc.PSYC import parsetext +class RoomGui(qt.QMainWindow, AbstractGui.RoomGui): + """qt frontend for pyPSYC""" + # better inherit from widget made with designer? + def __init__(self): + qt.QMainWindow.__init__(self) + #AbstractGui.RoomGui.__init__(self) # attribute error? + self.model = None + + self.rooms = {} + self.topics = {} + self.setCaption("Qt pyPSYC Frontend") + self.menuBar = qt.QMenuBar(self) + + self.workspace = qt.QVBox(self) + + self.roombar = qt.QTabWidget(self.workspace) + + # only in debug mode + self.roombar.addTab(MainDebugWidget(self.workspace), 'main') + + self.workspace.show() + self.setCentralWidget(self.workspace) + self.resize(qt.QSize(600, 500)) # fixed size is bad + + + fileMenu = [("Exit the program", "Quit", 0, "quit") + ] + connectionMenu = [("Link to your UNL", "Connect", 0, "connect") + ] + self.menus = [("&File", fileMenu), + ("&Connection", connectionMenu)] + self.menuActions = {} + for menu in self.menus: + popupMenu = qt.QPopupMenu(self) + for entry in menu[1]: + if entry: + helptxt, menutxt, accel, callbackName = entry + action = qt.QAction(helptxt, qt.QIconSet(qt.QPixmap("./green.gif")), + menutxt, accel, self) + action.addTo(popupMenu) + self.menuActions[callbackName] = action + callback = getattr(self, callbackName, None) + if callback: + qt.QObject.connect(action, SIGNAL("activated()"), + callback) + else: + popupMenu.insertSeparator() + self.menuBar.insertItem(menu[0], popupMenu) + + self.show() + + def update(self, mc, mmp, psyc): + context = mmp._query("_context") + if mc.startswith("_notice_place_enter"): + ## if _source == ME eigentlich... + if not self.rooms.has_key(context): + self.add_room(context) + self.rooms[context].append(parsetext(mmp, psyc)) + +# elif mc == "_status_place_topic": +# self.topics[context] = parsetext(mmp, psyc) + elif mc == '_message_public': + self.rooms[context].append(psyc._query("_nick") + ": " + + parsetext(mmp, psyc)) + + def add_room(self, room): + if not self.rooms.has_key(room): + self.rooms[room] = RoomWidget(self.workspace, room) + short = room[room.rfind("@"):] # kurzform für raum... nicht ideal? + self.roombar.addTab(self.rooms[room], short) + + def quit(self): + self.app.quit() + + def connect(self, host = ''): + self.model.center.connect(host) + +class UserGui(qt.QMainWindow, AbstractGui.UserGui): + pass + +class MainGui(qt.QApplication, AbstractGui.MainGui): + def __init__(self, argv, center): + qt.QApplication.__init__(self, argv) + AbstractGui.MainGui.__init__(self, argv) + + qt.QObject.connect(self, SIGNAL('lastWindowClosed()'), + self, SLOT('quit()')) + self.timer = qt.QTimer() + qt.QObject.connect(self.timer, SIGNAL("timeout()"), + self.socket_check) + + def socket_check(self): + asyncore.poll(timeout=0.0) + + def run(self): + self.timer.start(100) + self.exec_loop() diff --git a/pypsyc/GUI/Qt/__init__.py b/pypsyc/GUI/Qt/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/pypsyc/GUI/Qt/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/pypsyc/GUI/__init__.py b/pypsyc/GUI/__init__.py new file mode 100644 index 0000000..5d4eecb --- /dev/null +++ b/pypsyc/GUI/__init__.py @@ -0,0 +1 @@ +# different user interfaces for pyPSYC diff --git a/pypsyc/MMP/MMPPacket.py b/pypsyc/MMP/MMPPacket.py new file mode 100644 index 0000000..458dbee --- /dev/null +++ b/pypsyc/MMP/MMPPacket.py @@ -0,0 +1,28 @@ +from pypsyc.MMP import Glyphs + +#obsolete? +class MMPPacket: + """encapsulates a set of MMP state variables and data within a + MMP packet. See http://www.psyc.eu/mmp.html""" + # do subclassing for binary data packets and fragmented packets + + delimiter = '.\n' # end of packet delimiter + def generate(self, MMPModifiers, data): + """Expects a (emtpy) dictionary of MMP Modifiers and + a (empty) string of data. Does nothing with the data itself""" + packet = "" + packet += MMPModifiers.plain() + data = data.strip() + + # take a look data the first character of data. If it is no glyph + # we may leave out the newline before data + if data.__len__() and _isModifier(data[0]): + packet += '\n' + else: + # keys may be empty (so we either have an empty packet or we + # set default variables for communication + pass + + + # append data and EOP delimiter + return packet + data + '\n' + MMPPacket.delimiter diff --git a/pypsyc/MMP/MMPProtocol.py b/pypsyc/MMP/MMPProtocol.py new file mode 100644 index 0000000..41a3d79 --- /dev/null +++ b/pypsyc/MMP/MMPProtocol.py @@ -0,0 +1,84 @@ +import asynchat, asyncore, socket +from pypsyc.MMP import Glyphs, _isModifier +from pypsyc.MMP.MMPState import MMPState + +debug=0 + +class MMPProtocol(asynchat.async_chat): + """modular message protocol + see http://www.psyc.eu/mmp.html for details.""" + def __init__(self, callback, connection): + asynchat.async_chat.__init__(self, connection) + self.set_terminator('\n') + self.buffer = '' + self.mmp = MMPState() + self.state = 'mmp' + self.data = [] + + self.callback = callback + + def handle_connect(self): + # dead code neuerdings, eh? + print "connected... but this is dead code" + + def handle_close(self): + s = MMPState() + if self.addr[1] != 4404: + s._set('_source', "psyc://%s:%d/"%self.addr) + else: + s._set('_source', "psyc://%s/"%self.addr[0]) + self.packetReceived(s, "_notice_circuit_broken\nYour TCP connection to [_source] was broken by the bull from the china shop\n.\n") + self.close() + + def collect_incoming_data(self, data): + self.buffer += data + + def found_terminator(self): + # hierfuer siehe asyncchat.py... das setzt + # terminator auf 0 zurueck wenns nen int als terminator + # hat und den findet (line 111) + if self.get_terminator() == 0: + self.set_terminator("\n") + line = self.buffer + self.buffer = '' + self.lineReceived(line) + + def lineReceived(self, line): + # kind of state machine + line = line.strip() + if line == ".": +## print "<>" + self.packetReceived(self.mmp, self.data) + # reset state, clear temporary variables, etc + self.mmp.reset_state() + self.data = [] + self.state = 'mmp' + return + + if (line == '' or not _isModifier(line)) and self.state == 'mmp': + # if we have _length for data then + # binary support + if self.mmp._query('_length'): + self.set_terminator(self.mmp._query('_length')) + self.state = "data" +## print "<<" + self.state + ">> " + line ## comment out + if self.state == 'mmp': + # maybe else with the above could also work... + self.mmp.set_state(line) + return + elif line != '': + self.data += [line] + + def packetReceived(self, mmp, data): +## print "MMPProtocol::packetReceived" +## print "=>MMP :", mmp.get_state() +## print "=>data:", data + self.callback.packetReceived(mmp, data) + + def sendPacket(self, mmp, data): +## print "calling MMPProtocol::sendPacket()" + packet = mmp.plain() + packet += '\n'+ data +'\n.\n' + if debug: + print ">>send>>\n", packet, "<UEEXo{T-ZY*5~$VUzL}4O^u6pAEWfQ{JIr2XhFwC~c7@%6u9@N1J{n zx+Z!v?D8q+y$(*y{>10B=V55u=#KNnY+~#u7)F^nDbs9}>8ht#&ZeH6oSoa)>I`F& zZCE+0N?RsnzPc1|{|?Q(Mh6Sr71(&iMhqL2dNOFT)x6T8v?+r&8_g@7rNw-Wj;#2U zZpola;d#$z{Xe3cmgzi?t6-{)iN`vaEv@PLK@b?NgNe1X=Yzr3)m49H7D->v2h-`S zKe5w17?o9!Y8z*{=_d_ntJ@1!{|cQ2NQyF#J`jEI=-MNNQHCohx>i71BvF%Wi_wKF z!W|ki=+M}$Y|#fLE4Tvz`X&^g8iuJ0{Wye=g;BjOPZ;RfEQ(|jTgxjliDb&MD9Vb= zMv=XRiP1K)x|(K1Y|BcVcrhQ1bQQ8w!xlPNhJEZd%Lk`1hO8%v^ZX)CE_-~sh>Xrh z;SSIE*rY59o!G1_dYF-P<|~W_V_gh>8oVB)`u$+GU{vcL&|z?c68+gCoG!Ge8G5Q?_@Z&YnM{zRaL10DN zWEw!e&qOL7-4wN!B1Gx~5j&pYzX*VucTGNp!~ML+xSm24^nHCB25>w_rkq!aHta)o z3wZz3Jb#)F40~X;GLZN+2u+HL(q;+nnN`Ugth%VmOI>u|Ku_@M#TlTjNf6jF_<4E2 zFTqu80=GA~Sil<^Uda(qRXLSeG65q4Gbv}?ez(U^2_c1Mahg^#h`?R}dj=Y68|p-7 z?{zAKP4d!c$xPx;%NI2pd375aYOk{LPgx@`63BR09e4+7S0(IOZVfc??j8nkMH868 z7tg#%lJmJF#Z&1+26t_0$T~N&^d1x-q2`D_#ClR9y2la6?Pm};8Y+pM+E_947((WZ zFcAcN>rr{zr7LgUmb^+Ldcm#NjO32$@~zAC>bWy^-{uU`_Z<%{_IS6U%>iv4d-H7_P!pVw53z zUPI(?;l8noH;*zGa?4CEr3+gkR4m}ckqs1~W_F7dvaiE%yF779u?tB6hkR4Ro}sWo zqoVS$TS9FRf2;(xn@|YdLAB3T%I>X{`8SS09 z5aL{yKXP4+?Zh#dS-q?sRW=Jb1-k9*WGViY=JF$@CFD0l$Xt;c6uq14Y9Yk1%#PK* zH6g+StZzbw@F5iRS$Tktylu7P?R#8ZeAV*4Z0xDWSlRV0ehi?ofe zEAIjkU&OtOq#nxUrA|sRTEnoB{fy{KG#AlLv9O8xD)mT2uSkmsm1hU+yboI4fA+CK z@FK@JICu=03ZImlBfhf!K4Q6x{0kW)vvn>*;* zUR(KHzv=sa1HTXbwpbl<_^ls#Wu*l*Q*sHETev4xAMdCTTbEJAWoRv6p}fT1Gw%s+ dp;b%zehBMt?#h3<*kSG!)@`wK$i1Od z>8J9+Kj8Qm{5$>?|A0Qvxtm0W8HG~p_THScclVxip7V0g{_*ExZ>_n^nW?-GH$TU)zhW{%F_20rOR`>}ct}~V3Ph8D+(?PJQi1h(3g=Q? zwVIPB(6u4?yxu${d0mMG$s0;6N`6R*!;&v3aYXV(C6**Vti(~tk0^0WlC|bhs`nZv zY90^n-ZLE+Zwy8;tm)7;jtJumKJ*i&uE=ANbV>x1JK=-?X8xHfkt*h7cv*t3XJAqw zAM-YB3OxmjB2U4jil<;xRjG7N`gD)L3xvJxsL`i7pVRrg&KGsQ++4(}t8?lWR{h}` z;D%;o3+qhZxkF^L&VA$5Xm#dzn8SWo!BTtBH;(@9W!62+MhWFP0(Enc7aN5cT^sj1 zt{C*KK9*+I7MpzyfixR$ZczpmNb_Q==ohxD+osky6_q|1o6(MsBg(U;x6#C}=wv?t z)!H=u*7vRNTDf`H8tx43PG|B1x3ngKwmRJLQd5X=Y04n9Z*Z%i?ysmGHG(K;lvjt2 zoD6m7DcpD*(xC;2KrWR`pbT8K)?^g7u?huACWIy-q)qOWEMrm}098NRGOmgn53}Cb z?Axg1Ft#{}Vd-!Q7lXxMsye-)suN-;c>RWkhH7cw#Jz13+wNf8%j41+aTY_jBD>o& zih9BTm?@vU4mLT?dl>ujLUIHzzJp=uP>HI~sqR~Nc&6?oT>b}rmmv6k46kqa4}IxV zh4()qgXu_Dv^|g-6!gd|0GFxvA%>5Jh|%D{$}}YCd({~=whZM$19&fx!GZ+S=svNg zbSTd(BLxB}m!oHwqjH&{rs3vIXQ(H%S`(m~+4R2I(cDOwi?0BSW7zg149gUeU^!Tr zjB1CmM~yfXj>{l`6pSs@KEiQY2%fk9#BN0#OsfzyCVnKFk?e)C7BKUt*>B&&M%I|P z>)h~StF^toy~>)jYR0XtvL@QquG{Lhn+gfhNmz&zRwJzl*>Kp~@qp+fS~dvMWP@26 zYPsA%joo)Xx&>^EdYex2{t9_YlV};?vl4X}GgO2XM!|Xj0x^I>hl2!2!TNil3}PQX ziXYCa6L(_K{mpp0aNQV35f9?HYqH#o;$G1=+J!(%bs`X60o88O_a3*RpX=$f_SLPc z$<=SaNfH)C^?@Ds3g zf10-!!E{A-HECfR(NZT6+mc5=hBjcvhZFv(Iz&l6_o#(Gu+&xs$ZQQ?&aBOeS$D9z zH!G+GOM0nUjQput*TK*fZw=Cwj2550l0m8Qv4N1Fby%;c=~zdd0iU8PoYh(_vz*kQ zot{B^LPNdDYCI3Ogz@jsIvT%!o4MJ@Or6;QTUyO3%v4gV3GOtJegT8zfioXnc*^`Q ze>|fJp*i7`1_`m^Fi?3`kX9=f@YaLla=Uy}D>1No-ZyW_Z4aU1%jSiewKYc)jduE{ z;ZuOPDU5|PTQ=c&=S%k)Q2L_pZHT3B7kw5UM2b+CVJ|EC_dr+{bqQ+Wk}L$LDt>Nw zWS@#BR#e)j_#$4)-pqHQb}$}w$_9rU9)$ZDP#W^OH=BQpAvw6Idb_RWC9v!f1%o&1KZVrce)Y?rgL@GFCr<|txWs;^i0>?eF%gP0n&U$LTz$7N%JZQ0fIgGxQrs6DY=B zHPpOXP+zXsa8>Jb3ybwe{aAet*AhQG+FlrQ&wqRA7tAUSvIMupry3*s<&@HIpGzLR N&$yj-vrAzu{2Lv7-GKlA literal 0 HcmV?d00001 diff --git a/pypsyc/MMP/__init__.py b/pypsyc/MMP/__init__.py new file mode 100644 index 0000000..45e3d60 --- /dev/null +++ b/pypsyc/MMP/__init__.py @@ -0,0 +1,8 @@ +# modifier glyphs as defined in the whitepaper +Glyphs = ['=', '+', '-', ':', '?'] + +def _isModifier(str): + # W A R N I N G: duplicated code + # this is a hack... first part makes sure that name[0] is + # valid, second part does the actual check + return (len(str) and str[0] in Glyphs) diff --git a/pypsyc/MMP/__init__.pyc b/pypsyc/MMP/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a14e9c508a0795a0fd265e4584f9c5c3c5393f95 GIT binary patch literal 382 zcmbV_!Ait16h&`4?Kmj9b1}GcRYo*>QAcnws}3@P$ZWD`;uxuInM9%BPXEVW(Vy@G z`~q(tihD!ec{eYYcR#+iH$UIsrWJlp&VJ7F`-I_5U{aU?OommCB_}yvfhFfdTooI% zg@ia29taPG$HHk9e`BSr3zpv_0`LsHBxrY#P%&pr|9#vUjdMppWEfD@wh2S_S>1Pc zzKHu|?!&sEhQLp#)XFyMMqMjopH$a(zOQ~;SzaowUE_k*M_nJbXxGk{t#P-`t}{M; g3`aJX>>", target +# print "mmp :", mmp.get_state() +# print "psyc:", psyc.get_state() +# print "mc :", psyc.get_mc() +# print "text:", psyc.get_text() +# #print "get_host(" + unl + "):", get_host(unl) +# print "---" + + host = gethostbyname(get_host(target)) + if not self.clients.has_key(host): +## print "Error: we are not yet connected to ", host + self.connect(host) + # put the current packet in a queue, + # connect and auth before sending + else: + # wait... we have to register before sending? + # print "sending..." + # print "client is:", self.clients[hostpart] + self.clients[host].sendPacket(mmp, psyc) + + def quit(self): + # "unlink" waer der bessere name? + print "unlinking world..." + for host in self.connections.keys(): + # auch nicht grade grateful... request unlink + # waer schoener + del self.clients[host] + + def register(self, psycPackage, methods = []): + """see bitkoenigs psycpackage interface... that's the way to + do it""" + if methods == []: + methods = psycPackage.getMethods() + for method in methods: + # jetzt auch support fuer mehrere packages pro mc + if not self.handlers.has_key(method): + self.handlers[method] = [ psycPackage ] + else: + self.handlers[method] += [ psycPackage ] + psycPackage.registerCenter(self) + + def castmsg(self, mmp, psyc): + # ob das noch/ueberhaupt noetig ist... + """for internal packets""" + self.handle(mmp, psyc) + + def handle(self, mmp, psyc): + context = mmp._query("_context") + if context: + if not context.startswith("psyc://") and context.startswith("@"): + context = "psyc://" + self.DEFAULT_HOST()+"/"+context + mmp._set("_context", context) + + #if self.handlers.has_key(psyc.get_mc()): + # for package in self.handlers[psyc.get_mc()]: + # package.received(mmp.get_source(), + # psyc.get_mc(), + # mmp, psyc) + + method = psyc.get_mc() + source = mmp.get_source() + found = 0 + for handler in self.handlers.keys(): + if handler.endswith("*"): + tmp = handler.replace("*", "") + if method.startswith(tmp): + found = 1 + for package in self.handlers[handler]: + package.received(source, method, mmp, psyc) + else: + if handler == method: + found = 1 + for package in self.handlers[method]: + package.received(source, method, mmp, psyc) + + if not found: + if self.handlers.has_key("devel"): #(an) was elif + for package in self.handlers["devel"]: + package.received(source, method, mmp, psyc) + + + +class PSYCServercenter: + def __init__(self): + self.clients = {} + + def handle(self, mmp, psyc): + # das problem ist, dass wir die Klasse von der + # das kommt auch kennen muessen + print mmp.get_target() + diff --git a/pypsyc/PSYC/PSYCMessagecenter.pyc b/pypsyc/PSYC/PSYCMessagecenter.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c716d41d740ff2dfd28b30eedc5e3d5fa16c313 GIT binary patch literal 6520 zcmcgw?Q$GP6}>&X+7DT>Wy_AG#K77lM3^8^DFcR5C>%+SL+p(iIT))jTkDzDYS3zD zwbQaK%Hogp3KYLd6%@rAz$4^e9)VZj1>l_9vpZ`i6)1T@rS9$N>FK_G@44sR?$7@@ zJAM5Rf4Lb+{#V2APf@bJqKffvWLKnXWXp)*b0E8cbW5^Tl5SbH%D%TGyHnDw$X3Na zmu0sqTUDI_?J3!sl6XpNRg#))8;L7&Eb?6B!B$=3>e!3g=*6_ecsDEY^r-8U#P~lg z@yw`eM&hz;E}en6$-nToWYBUmsP1k)Si3t&T^gm`rGUNMT*tcBPMSGa&ma2Tc4Ylm zv%ZV3A077Ed$#G$p^Fo3rqQl-x0B4-o*j(TRmSW0QL=BLvO+C+rP)A|618;k{XGNkZv{*_gYbeMWekCZBsST#55vH(_D#4X=jJMC>>bGo;b<+Z5Qp>fxc_C zl3wCkEl2G{1KW1Cm8mvd1wDDQisp@*tq+?U_uOfAL(Kzwkl9u{jt3B2ZiB)zD)k??qR(`qi ziZvwJ7DkVoQ=qA35$Bg_fM*Z>sD-7+D3-e@@0nA(l-a*VtP z(NzRo zO55HlgRHQ6$kBmwpGMRU)iAj3}Q*NLPkx2PyY5uB=e@6o{Lcr&u zrYfmj)p9fGZJ5+|Xe!^I8$GKb(|2IBVGP0d7eQDT@xevf9Frme!Vy6Mq6UU5I^W@u zfv5vyR3yQP0S5^n*C{BJ5~p8kVrIRJHEy9~%%dfkF=tJ~ESU54b2uuTtwvM=iG;GOp=8_? zEcL;yryVE|De}Rdb>%l(|D2&y7BVzvOh>LHOd2V zlJ+v)FkCPT71qjQJ*@uYp&aOrfT_>0p<;+p=j zo`d&}bvtRCg`_k^rJi&Lw!yc1rKT(%e9GCtgSw==ciD7`t}6)QS*VJ7|e4r zVn9l>ILTI}63m!>16KP$Ku{p%0^CP!ddz4O9_ z7eq^c4H@u^oKcfXMsej`#Pw%29dR9Kt_f#b$!Eg{i|Y?B$q|^iE(wmBdIy9p(`LAU z+y95$-)|bT|9hPstj(O^OC|!HfAvIH&3#USa{pOH`X6FGA1rX|R#)-f6*CeHUH@Ue z-(tQ`O_6BOZcTsd02XuhsT+?Y;rH-5WD*BRg6_$vSACMiCc+p9)jqaeO|BJzt^ET# zI1HIoX%Gp2z=4cV3^sd_h72~d^k5KK@4BlTL--(+X#7#v99X>x%$+H+$zz|)sxf(2R4y9sFqkRS0)38;=Js0~_oiCr^mvmuMHq#r<{eGR?2m@N!?TMtr!{3txTd&nh&oziFdt$L`^{jvib>VlL$RV@u-c hPSYC :", self.state.get_state() +## print "=>mc :", self.state.get_mc() +## print "=>text :", self.state.get_text() +## print "----" + self.callback.handle(mmp, self.state) + diff --git a/pypsyc/PSYC/PSYCProtocol.pyc b/pypsyc/PSYC/PSYCProtocol.pyc new file mode 100644 index 0000000000000000000000000000000000000000..251b787c8713e410250e6a1ad38b813cda7a05c0 GIT binary patch literal 1727 zcmb_c!EVz)5S_K1q$w=~Q4tjhgn&Z?s5v4P1PGAIrB&rf6e?9#us6lTvD126S}8ph z{(zt0$fqFj1N;I%0PpR_$(<6}lbP}E?96+!^W*pO;@2-fpT#o$)$#uvZ@B@H;88LZ z$wYQV6m3t2N-|G&J!kte^d$>qH-H^T(vYMkpGD3^-tE>TsY|c3fLQ4t{5lGnX+muG z-o5OZzR}Kg39cwDH;W`4r225~+}zyWJ}gWT7nxa_?yDTLq#5-E-tsPlMy`sfBMBsl z0;u@RoJt^Juj6A~Oi{frx%&acM9BoHAWOv(sC#03Dicr6J?lZ4g0gt*R;LDOSYh1H zviJRX7(y3AcgX@nk0uWqjmGw56Y!hP1|bzg$jc%gYSV!xR+L?6-F=;R-*(@06Mftr zpN`AZxcVb}U5W~ewehJ8o_vEZ56ARj;Chf$RHvCXm+eskpN7#0-Dmz%g-4?3KKlK4D5w62QsV^yZ16rRD zvX6Qew=IJ#2(`$1N!he=(Sgc7+1DnrFj|;sjbd9URijT#$g0dDM5*&+8~6i9`a(kB zz7l4lXMs=pV1=H+QI=U=j{AofLq5EMaO}>q1tDG?h&QCJ?x+>ja@Z618TP+m5EeWZ zI^~Es`A#%n?R^t4VbE%UoGU5ri6Rq9K6+BLVYr<@)_zw9Hw2G@1&{%H@z@&Z_f_!5 zP4``dKtB3X+?+)K!!81-BcancI14MWZDqEyUajOoxZfMtxbYgs4wGp}x-h*sY@~`{ zlCTvL5qPI8M;3YVR$hbeC?Dp6q<}&0s ziZY!?k-JB(LAbL&9-|>#kIe4bhkl-9+P2rcKf> self.view, "devel handler:" + print >> self.view, "source=>", source + print >> self.view, "target=>", mmp.get_target() + print >> self.view, "mmp =>", mmp.get_state() + print >> self.view, "psyc =>", psyc.get_state() + print >> self.view, "mc =>", mc + print >> self.view, "text =>", parsetext(mmp, psyc) + print >> self.view, "----\n" + + +class Conferencing(PSYCPackage): + # model for rooms. has to know its controller and view + # the view is an instance of RoomGui (singleton in case of Qt GUI) + def __init__(self, view): + PSYCPackage.__init__(self) + self.packagename = "package for rooms" + self.view = view + self.model = self + self.view.set_model(self.model) + self.controller = self + + self.context = '' + + # ideal hier waere es wenn man auf _notice_place_enter* registern + # koennte, oder? + self.methods = ["_notice_place*", + "_notice_person_present_netburp", + "_notice_person_absent_netburp", + "_message*", + "_status_place*", + "_status_person_present_netburp", + "_print_status_place*"] + + def get_context(self): return self.context + def set_context(self, context): self.context = context + + def received(self, source, mc, mmp, psyc): + #if mc.startswith("_message_public"): + # self.view.received(source, mc, mmp, psyc) + #elif mc == "_status_place_topic": + # self.view.received(source, mc, mmp, psyc) + #elif mc.startswith("_notice_place_enter"): + # self.view.received(source, mc, mmp, psyc) + #elif mc.startswith("_notice_place_leave"): + # self.view.received(source, mc, mmp, psyc) + #elif mc == "_print_status_place_members": + # print parsetext(mmp, psyc) + #else: + # PSYCPackage.received(self, source, mc, mmp, psyc) + self.view.received(source, mc, mmp, psyc) + PSYCPackage.received(self, source, mc, mmp, psyc) + +class Friends(PSYCPackage): + def __init__(self, view): + PSYCPackage.__init__(self) + self.packagename = "package for friends" + self.view = view + self.model = self + self.view.set_model(self.model) + self.controller = self + self.methods = ["_notice_friend_present", + "_notice_friend_absent", + "_list_friends_present", # create this! +# "_print_notice_friend_present", # _print family should be obsolete now +# "_print_notice_friend_absent", # _print family should be obsolete now + "_print_list_friends_present", # obsolete this! + "_notice_link", + "_notice_unlink", + "_status_linked"] # added by tim] + + def received(self, source, mc, mmp, psyc): + self.view.received(source, mc, mmp, psyc) + PSYCPackage.received(self, source, mc, mmp, psyc) + + +class User(PSYCPackage): + def __init__(self, view): + PSYCPackage.__init__(self) + self.packagename = "package for user dialogues" + self.view = view + self.model = self + + self.view.set_model(self.model) + + self.controller = self + + self.methods = ["_internal_message_private_window_popup", + "_message*", + "_notice_query*"] + + + def received(self, source, mc, mmp, psyc): + self.view.received(source, mc, mmp, psyc) + PSYCPackage.received(self, source, mc, mmp, psyc) + +class Peer(User): + # is das ueberhaupt sinnvoll _hier_? + def __init__(self, view): + User.__init__(self, view) + self.packagename = "package for p2p dialogues" + + +from pypsyc.MMP.MMPState import MMPState +from pypsyc.PSYC.PSYCState import PSYCState +class Authentication(PSYCPackage): + # das hier sollte noch das setzen von _identification regeln oder? + def __init__(self, config): + PSYCPackage.__init__(self) + self.config = config + self.methods = ["_query_password", + "_notice_circuit_established", + "_error_illegal_name", + "_error_illegal_password", + "_error_invalid_password"] + + def auth(self): + psyc = PSYCState() + mmp = MMPState() + if self.center: + # das hier ist sicher weils nur an die eigene uni geht + mmp.set_state(":_target\t" + self.config.get("main", "uni")) + psyc.set_state(":_password\t" + self.config.get("main", + "password")) + psyc.set_mc("_set_password") + + self.center.send(mmp, psyc) + + def register(self): + psyc = PSYCState() + mmp = MMPState() + if self.center: + mmp.set_state(":_target\t" + self.config.get("main", "uni")) + psyc.set_mc("_request_link") + self.center.send(mmp, psyc) + + def received(self, source, mc, mmp, psyc): + if mc == "_query_password": + self.auth() + elif mc == "_notice_circuit_established": + self.register() + else: PSYCPackage.received(self, source, mc, mmp, psyc) diff --git a/pypsyc/PSYC/PSYCRoom.pyc b/pypsyc/PSYC/PSYCRoom.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ce6d247afa9ec37fe258038427d1f434e7e6583 GIT binary patch literal 8676 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 literal 0 HcmV?d00001 diff --git a/pypsyc/PSYC/PSYCState.py b/pypsyc/PSYC/PSYCState.py new file mode 100644 index 0000000..6bee923 --- /dev/null +++ b/pypsyc/PSYC/PSYCState.py @@ -0,0 +1,29 @@ +from pypsyc.MMP.MMPState import MMPState + +class PSYCState(MMPState): + def __init__(self): + self.mc = '' + self.text = '' + MMPState.__init__(self) + def set_mc(self, mc): + self.mc = mc + def get_mc(self): + return self.mc + def reset_mc(self): + self.mc = '' + def append_text(self, text): + if self.text != '': + self.text += '\n' + self.text += text + def get_text(self): + return self.text + def reset_text(self): + self.text = '' + def reset_state(self): + self.mc = '' + self.text = '' + MMPState.reset_state(self) + def get_source(self): raise "Meep. No source in PSYC Layer" + def get_target(self): raise "Meep. No target in PSYC Layer" + def get_context(self): raise "Meep. No context in PSYC Layer" + def get_length(self): raise "Meep. No length in PSYC Layer" diff --git a/pypsyc/PSYC/PSYCState.pyc b/pypsyc/PSYC/PSYCState.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40b1021d61464ca431a7282b10ea2318522a718f GIT binary patch literal 2584 zcmcIm&2AGh5FT%~NjGhwP=pYFiquPlP;-L|0z|JgsvHT?URKfCErDbc#V#UJPZZvO z7enGncmbGiyxEirP}8PKW_Ij#WXb#aJb? zBE}l&PIHw5XMh2TShiW*CM%AAOq7;}A|Fw&g2@x)fq2LGLmSws8MERAM1#zWZ9jR9 zJCcVq?d3h6rXe%9?`;2|iTk45W42m{HgCOcy=fWyu{D~E+$5V`kh)fxzpc?kBiZ7o z3MVSzZ-($wk$tT3#BsR~AjUS^qIHE$*@1?OFl6I`fIOTy>(fC7;vI01DpQNN0;$FZ zr?B@oFjypDE|D0)aVR+I?CvZMz$#NSqqrnfW5KEXECdn~GPWT(D7Y!8DOX@ZKs{k2 zB!{8Buh4LV6pv%h7N3Hr5m#f*wO33rvod9X9zNQF-T`wx>;V;Wienk_)I~X7;lFlA zBb%GlbA4aqp$ql5t~Sn3w!m0eWh4t^dK=Ufw|eYP{}F>^Q($NCf}@Dj*)w`PNcp&W=~g+G=#Y~ zC(?(!zRYPh%u!lb1{L*$q!w}R&x!R9{`)pR^zW_=>XiQw29bsTCVErx=*_zWo2DKs z({wO2M|~d3161Q_q|u%=UQjfaXl!1iP0I<;aHOw8twY^lH9J*e&HwO5QKDhHJFKcI xiIYga;Tq$pC8{kw-EZ)c@G`K)Z&MI?hmv;-4M&$nBUySD^70%GrHQUbzX9v5*FyjR literal 0 HcmV?d00001 diff --git a/pypsyc/PSYC/__init__.py b/pypsyc/PSYC/__init__.py new file mode 100644 index 0000000..c7b2a8d --- /dev/null +++ b/pypsyc/PSYC/__init__.py @@ -0,0 +1,38 @@ +def parsetext(mmp, psyc): + """string with variables of the form '[_varname]' to be substituted + by the value dictVariables['_varname']""" + pstring = psyc.get_text() + for (varname, value) in mmp.get_state().items(): + pstring = pstring.replace('[' + varname + ']', value) + for (varname, value) in psyc.get_state().items(): + pstring = pstring.replace('[' + varname + ']', value) + return pstring + +def get_host(uni): + import re + + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/(.+)?\/\@(.+)?\/?$", uni) + if m: return m.group(1) + + m = re.match("^psyc:\/\/(.+)$", uni) + if m: return m.group(1) + +def get_place(uni): + import re + m = re.match("^psyc:\/\/(.+)?\/\@(.+)?\/?$", uni) + if m: return m.group(2) + +def get_user(uni): + import re + m = re.match("^psyc:\/\/(.+)?\/~(.+)?\/?$", uni) + if m: return m.group(2) + + m = re.match("^psyc:\/\/([^\/@]+)\@(.+?)\/?$", uni) + if m: return m.group(1) + diff --git a/pypsyc/PSYC/__init__.pyc b/pypsyc/PSYC/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91c0522ec01d72c877e390cc5193e5d2ef54e7b1 GIT binary patch literal 1778 zcmcgs!EVz)5S_KtmXx%G_D~5>>qA8nRjQ9j6>2J#bA=MA1>!Vv;%%}bj$QA%G*ZGP zocI=ghX3Hi55UaW5mW)Sv=Wi_?KnF-o_+7FzkXj?o?d)A=tBMzI3MEB6@&ob6TmwR z1P1~FivtS+n*$qy0yHWMnB27xC|JaCghPKu7z)}*0YXojwvkuH6!Qmdd?O8!r;H4>S;8oh@xbp&=GIW}VHg`K z5$jQxGve$Z9OvzF4ep8b0|vc|k=v6)_l7fca@%b+RHohDi$~m5 z>?c}Mk8}Y2h>F=Iu>mEqm1CDys4wd22#5ZHfbsQRfOz6A~Rbco8E(u*=^)|M_g!P2ni8H$o%ooaPbbuzop-qCHj`$ z(xL3UTR4=Q{2$1inJd*Y*~V9D5^Tn0X0BuYBG$=z^~VJ31RDsyMC9n7zhLz8mthp7 R19`wVmv}5L7uSjlKLOEnU<&{M literal 0 HcmV?d00001 diff --git a/pypsyc/__init__.py b/pypsyc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypsyc/__init__.pyc b/pypsyc/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86da0c365569ec9559b142369b4aab60020d5a78 GIT binary patch literal 125 zcmcckiI+>cB+V|F0SXv_v;ztherapy-big-big-big + -mv -f therapy /tmp + -mv -f therapy-big-big-big therapy + python -tt therapy + +# paranoid check of syntax +jack: pypsyc therapy + python -tt therapy + +pypsyc: + @echo I will presume you have also checked out pypsyc. + ln -s ../pypsyc/pypsyc . + +it: jack + diff --git a/therapy/README b/therapy/README new file mode 100644 index 0000000..426e702 --- /dev/null +++ b/therapy/README @@ -0,0 +1,10 @@ +$Id: README,v 1.2 2003/09/22 18:54:51 an Exp $ + +pypsyc was slightly changed for therapy. Hopefully, i'll get that clean one +day. + +To run therapy, you need Python (tested with 2.3), pypsyc (provided in +this Package), PyGTK 1.99 and GTK+ >2.0. You can obtain PyGTK from +http://www.daa.com.au/~james/software/pygtk/. + +Have fun. diff --git a/therapy/TODO b/therapy/TODO new file mode 100644 index 0000000..72b2038 --- /dev/null +++ b/therapy/TODO @@ -0,0 +1,31 @@ +BUGS: + +fippo hat alles wieder zerschossen... ;) + error: uncaptured python exception, closing channel (exceptions.TypeError:not enough arguments for format string [/usr/lib/python2.3/asyncore.py|read|69] [/usr/lib/python2.3/asyncore.py|handle_read_event|390] [/usr/lib/python2.3/asynchat.py|handle_read|105] [/V/dev/therapy/pypsyc/MixedReceiver.py|collect_incoming_data|44] [/V/dev/therapy/pypsyc/PSYC.py|lineReceived|42] [/V/dev/therapy/pypsyc/PSYC.py|packetReceived|66] [/V/dev/therapy/pypsyc/center.py|msg|207] [./therapy|msg|404]) + say( hullo ) + error: uncaptured python exception, closing channel (socket.error:(9, 'Bad file descriptor') [/usr/lib/python2.3/asynchat.py|initiate_send|218] [/usr/lib/python2.3/asyncore.py|send|337]) + TypeError: main_quit() takes no arguments (2 given) + +_message_invite + +/me fixen + +Tab-Leiste im GTK-Interface: Scroller, wenn Tabs breiter als Fenster sind + +/all in GoodAdvice tuts nicht: _target mitgeben + + +FEATURES: + +farben via config einstellbar +fonts via config einstellbar + +zusaetzliche UIs (wx, curses etc.) + +Userliste in Raeumen + +Friendlist + +Floating Windows + +anclickbare links diff --git a/therapy/therapy b/therapy/therapy new file mode 100755 index 0000000..70f4d4f --- /dev/null +++ b/therapy/therapy @@ -0,0 +1,629 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +# +# theraPY +# A simple Chat Software for PSYC +# +# Copyright (c) 2003,2004 by Andreas Neue +# +# $Id: therapy,v 1.43 2005/02/10 21:28:23 fippo Exp $ + +try: + from twisted.internet import gtk2reactor + gtk2reactor.install() + from twisted.internet import reactor, task + twisting = True +except ImportError: + import asyncore + twisting = False + +import gc + +import os, sys, time, string, socket +import gtk, pango +import ConfigParser + +from pypsyc.objects import PSYCObject +from pypsyc.center import Center +from pypsyc.center import Client, Authenticator +from pypsyc import parsetext, parseURL + +conn = None + +nl = '\n' + +release = "050119" +infotext = "theraPY/" + release + """ +(c) 2003/04 by Andreas "an" Neue +Based on pyPSYC by Philipp "fippo" Hancke +Thanks to mju for conceiving the right name at the right time + +This software is licensed under the terms of the GPL. +""" + +banner = '* ' + +config = None + +main_window = None +status_window = None +authenticator = None + +tab_windows = [] + + +class TabWindow(PSYCObject): + + def __init__(self, netname, label, config, center): + global tab_windows + + PSYCObject.__init__(self, netname, center) + + self.netname = netname + + self.vbox = gtk.VBox(gtk.FALSE, 0) + self.vbox.show() + + self.hbox = gtk.HBox(gtk.FALSE, 0) + self.hbox.show() + + self.vbox.pack_start(self.hbox, gtk.FALSE, gtk.FALSE, 0) + + self.infobar = gtk.Button(netname) + self.hbox.pack_start(self.infobar, gtk.TRUE, gtk.TRUE, 0) + self.infobar.show() + + self.closer = gtk.Button('X') + self.hbox.pack_start(self.closer, gtk.FALSE, gtk.FALSE, 0) + self.closer.show() + self.closer.connect('clicked', self.delete) + + self.scrollwin = gtk.ScrolledWindow() + self.scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) + + self.textview = gtk.TextView() + self.textview.set_cursor_visible(gtk.FALSE) + self.textview.set_wrap_mode(gtk.WRAP_WORD) + self.textview.set_editable(gtk.FALSE) + self.textbuf = self.textview.get_buffer() + + self.scrollwin.add(self.textview) + self.scrollwin.show() + + font = pango.FontDescription(config.get('ui', 'font')) + self.textview.modify_font(font) + self.textview.show() + + self.label = label + if netname.find('@') != -1: + self.label_text= netname[netname.rfind('@'):] + elif netname.find('~') != -1: + self.label_text = netname[netname.rfind('~')+1:] + else: + self.label_text = netname + + self.label.set_text(self.label_text + ' *') + + self.vbox.pack_start(self.scrollwin, gtk.TRUE, gtk.TRUE, 0) + + tab_windows.append(self) + def get_frame(self): + return self.vbox + def __del__(self): + pass + + def delete(self, foo): + global tab_windows + + num = main_window.notebook.page_num(self.vbox) + main_window.notebook.remove_page(num) + tab_windows.remove(self) + + def append_text(self, text): + #iter = self.textbuf.get_end_iter() + #self.textbuf.insert(iter, text) + #iter = self.textbuf.get_end_iter() + #self.textbuf.place_cursor(iter) + #mark = self.textbuf.get_insert() + ##mark = self.textbuf.get_mark_at_iter(iter) + #self.textview.scroll_mark_onscreen(mark) + + iter = self.textbuf.get_end_iter() + text = text.encode('utf-8') + '\n' + self.textbuf.insert(iter, text) + iter = self.textbuf.get_end_iter() + self.textbuf.place_cursor(iter) + mark = self.textbuf.get_insert() + self.textview.scroll_mark_onscreen(mark) + + cur_page = main_window.notebook.get_current_page() + win = None + for w in tab_windows: + if main_window.notebook.page_num(w.get_frame()) == cur_page: + win = w + if win != self: + #self.label.modify_fg(gtk.STATE_NORMAL, + # self.label.get_colormap().alloc_color('darkred')) + self.label.set_text(self.label_text + ' *') + + +class MainWindow(Client): + + def __init__(self, config, entry_handler): + global main_window + Client.__init__(self, config) + + self.config = config + + self.entry_handler = entry_handler + + # create a new window + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_size_request(350, 400) + self.window.set_title('theraPY') + self.window.connect('delete_event', lambda e, w: gtk.main_quit()) + + self.vbox = gtk.VBox(gtk.FALSE, 0) + self.window.add(self.vbox) + self.vbox.show() + + self.notebook = gtk.Notebook() + self.notebook.set_tab_pos(gtk.POS_BOTTOM) + # window.add(notebook) + self.vbox.pack_start(self.notebook, gtk.TRUE, gtk.TRUE, 0) + self.notebook.show() + + self.entry = gtk.Entry() + self.entry.set_max_length(256) + self.entry.connect('activate', self.enter_callback, self.entry) + self.entry.set_text('') + self.vbox.pack_start(self.entry, gtk.FALSE, gtk.FALSE, 0) + self.font = pango.FontDescription(config.get('ui', 'font')) + self.entry.modify_font(self.font) + self.entry.show() + + self.statusbar = gtk.Statusbar() + self.vbox.pack_start(self.statusbar, gtk.FALSE, gtk.FALSE, 0) + self.statusbar.show() + + self.window.connect('key_press_event', self.key_press_callback) + self.window.show() + + main_window = self + + def create_user(self, netname): + label = gtk.Label() + win = QueryWindow(netname, label, self.config, self) + self.notebook.append_page(win.get_frame(), label) + return win + + def create_place(self, netname): + label = gtk.Label() + win = PlaceWindow(netname, label, self.config, self) + self.notebook.append_page(win.get_frame(), label) + return win + + def key_press_callback(self, widget, event, *args): + key = event.keyval + if event.state & gtk.gdk.MOD1_MASK: + if key > 47 and key < 58: + if key == 48: + key += 10 + key -= 49 + try: + self.notebook.set_current_page(key) + except: + pass + return 1 + elif key == 110: + self.notebook.next_page() + return 1 + elif key == 112: + self.notebook.prev_page() + return 0 + + def enter_callback(self, widget, entry): + global tab_windows + + text = entry.get_text() + + cur_page = main_window.notebook.get_current_page() + netname = None + for w in tab_windows: + if main_window.notebook.page_num(w.vbox) == cur_page: + netname = w.netname + win = w + + self.entry_handler(win, netname, text) + + entry.set_text('') + + + +def update_notebook_tabs(): + cur_page = main_window.notebook.get_current_page() + win = None + for w in tab_windows: + if main_window.notebook.page_num(w.vbox) == cur_page: + win = w + if not win: + return + + #win.label.modify_fg(gtk.STATE_NORMAL, + # win.label.get_colormap().alloc_color('black')) + win.label.set_text(win.label_text) + + if not main_window.entry.flags() & gtk.HAS_FOCUS: + i1 = win.textbuf.get_iter_at_mark(win.textbuf.get_insert()) + i2 = win.textbuf.get_iter_at_mark(win.textbuf.get_selection_bound()) + if not i1.compare(i2): + main_window.entry.grab_focus() + p = main_window.entry.get_text().__len__() + main_window.entry.select_region(p, p) + + +class StatusWindow(TabWindow): + + def __init__(self, netname, label, config, center): + self.conn = conn + TabWindow.__init__(self, netname, label, config, center) + + def msg(self, vars, mc, data, caller): + if mc.startswith('_query_password'): + authenticator.authenticate(vars, mc, data, caller) + return + if mc.startswith('_notice'): + self.append_text(banner + parsetext(vars, mc, data)) + return + if mc.startswith('_status'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_info'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_warning'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_failure'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + PSYCObject.msg(self, vars, mc, data, caller) + + def say(self, text): + #text = text.encode(self.config.get('main', 'encoding')) + print "input(", text, ")" + self.sendmsg({'_target': self.netname, + '_identification': config.get('main', 'uni'), + }, '_request_input', text) + + + +class QueryWindow(TabWindow): + + def __init__(self, netname, label, config, center): + TabWindow.__init__(self, netname, label, config, center) + self.netname = netname + self.config = config + + def msg(self, vars, mc, data, caller): + nickaction = vars['_nick'] + try: + nickaction = nickaction + " " + vars['_action'] + except: + pass + + #template = self.config.get('ui', 'msgtemplate') + + if mc == '_message_public': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_private': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_echo_private': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_private_question': + nickaction += " fragt" + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_private_ask': + nickaction += " fragt" + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_private_text_action': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_status_place_members': + # _list_members, _list_members_nicks + for i in range(0, len(vars['_list_members'])): + self.nicklist.append((vars['_list_members'][i], + vars['_list_members_nicks'][i])) + return + + if mc.startswith('_notice_place_leave'): + return + + if mc.startswith('_notice_place_enter'): + self.nicklist.append((vars['_source'], vars['_nick'])) + return + + if mc.startswith('_status'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_info'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_notice'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_failure'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_error'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_warning'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + PSYCObject.msg(self, vars, mc, data, caller) + + def say(self, text): + me = config.get('main', 'uni') + text = text.encode(self.config.get('main', 'encoding')) + print "talk(", text, ")" + self.sendmsg({'_target': self.netname, + '_identification': config.get('main', 'uni'), + '_nick': config.get('main', 'nick')}, + '_message_private', text) + + + +class PlaceWindow(TabWindow): + + def __init__(self, netname, label, config, center): + TabWindow.__init__(self, netname, label, config, center) + self.nicklist = [] + self.netname = netname + self.config = config + + def msg(self, vars, mc, data, caller): + #print "got PlaceWindow.msg(", mc, ") from", caller + nickaction = vars['_nick'] + try: + nickaction = nickaction + " " + vars['_action'] + except: + pass + + #template = self.config.get('ui', 'msgtemplate') + + if mc == '_message_public': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_private': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_echo_public': + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_public_question': + nickaction += " fragt" + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_public_ask': + nickaction += " fragt" + self.append_text('%s: %s' % (nickaction, data)) + return + + if mc == '_message_public_text_action': + self.append_text(template % (nickaction, data)) + return + + if mc == '_status_place_members': + # _list_members, _list_members_nicks + for i in range(0, len(vars['_list_members'])): + self.nicklist.append((vars['_list_members'][i], + vars['_list_members_nicks'][i])) + return + + if mc.startswith('_notice_place_leave'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_notice_place_enter'): + #self.nicklist.append((vars['_source'], vars['_nick'])) + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_echo_place_enter'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_echo_place_leave'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_status'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_info'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_notice'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_failure'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_error'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + if mc.startswith('_warning'): + self.append_text(banner + parsetext(vars, mc, data)) + return + + PSYCObject.msg(self, vars, mc, data, caller) + + def say(self, text): + text = text.encode(self.config.get('main', 'encoding')) + print "say(", text, ")" + self.sendmsg({'_target': self.netname, + '_identification': config.get('main', 'uni'), + '_nick': config.get('main', 'nick')}, + '_message_public', text) + + + + +def poll(): + asyncore.poll(timeout = 0.0) + gtk.timeout_add(250, poll) + update_notebook_tabs() + +def cmd(win, text): + #text = text.encode(win.config.get('main', 'encoding')) + print "cmd(", text, ")" + win.sendmsg({'_target': win.netname, + '_identification': config.get('main', 'uni'), + }, '_request_execute', text) + +def entry_handler(win, netname, text): + global config + """ + if text and text[0] == "/": + win.sendmsg({'_target': config.get('main', 'uni')}, + '_request_execute', text) + else: + win.sendmsg({'_target': config.get('main', 'uni')}, + '_request_execute', + '/msg ' + win.netname + ' ' + text) + """ + if text and text[0] == '/': + if text.startswith('/q ') or \ + text.startswith('/query '): + user = text.split(' ')[1] + u = parseURL(config.get('main', 'uni')) + if user.find('~') == -1: + user = 'psyc://' + u['host'] + '/~' + user + elif user[0] == '~': + user = 'psyc://' + u['host'] + '/' + user + QueryWindow(user, None, config, main_window) + + elif text.startswith('/j ') or \ + text.startswith('/join '): + place = text.split(' ')[1] + u = parseURL(config.get('main', 'uni')) + if place.find('@') == -1: + place = 'psyc://' + u['host'] + '/@' + place + elif place[0] == '@': + place = 'psyc://' + u['host'] + '/' + place + print 'join', place + win.sendmsg({'_target': place, + '_nick': config.get('main', 'nick')}, + '_request_enter', '') + + elif text == '/pa' or \ + text == '/part': + try: + win.sendmsg({'_target' : win.netname}, + '_request_leave', '') + except: + print "failed to leave current place" + + elif text.startswith('/pa ') or \ + text.startswith('/part '): + try: + win.sendmsg({'_target' : text.split(' ')[1]}, + '_request_leave', '') + except: + print "failed to leave that place" + +# elif text.startswith('/quit'): +# pass +# +# elif text.startswith('/m '): +# pass +# +# elif text.startswith('/me '): +# pass +# + else: + try: + # should use the text[1..] really! TODO + cmd(win, text) + except: + pass + + elif text: + #print "calling say(", text, ") in", win + win.say(text) + + + + +if __name__ == '__main__': + #gc.set_debug(gc.DEBUG_LEAK) + + # pypsyc should one day implement http://psyc.pages.de/storage + # but that's something we can look into on some much later day. + # in the meantime we could provide for defaults if therapyrc + # is missing. what about.. psyc://beta.ve.symlynx.com/~$USER + config = ConfigParser.ConfigParser() + if len(sys.argv) == 2: + config_file = sys.argv[1] + else: + config_file = os.getenv('HOME') + '/.psyc/therapyrc' + config.read(config_file) + + main_window = MainWindow(config, entry_handler) + + authenticator = Authenticator(main_window, + config.get('main', 'uni'), + config.get('main', 'password')) + + status_label = gtk.Label('Status') + status_window = StatusWindow(config.get('main', 'uni'), status_label, + config, main_window) + main_window.notebook.append_page(status_window.get_frame(), status_label) + status_window.append_text(infotext) + + main_window.online() + + if not twisting: + poll() + gtk.main() + else: + l = task.LoopingCall(update_notebook_tabs) + l.start(0.25) # 250msec + reactor.run() + + print "cleaning up" + cmd(status_window, "QUIT") + + +# vim:expandtab diff --git a/therapy/therapyrc-dist b/therapy/therapyrc-dist new file mode 100644 index 0000000..59eb740 --- /dev/null +++ b/therapy/therapyrc-dist @@ -0,0 +1,21 @@ +# ~/.therapyrc +# Use this file as base for your configuration +# +# $Id: therapyrc-dist,v 1.8 2005/12/07 10:26:24 lynx Exp $ + +[main] +uni: psyc://psyced.org/~an +port: 4404 +password: supergeheim +nick: an +encoding: iso-8859-1 + +[ui] +font: Courier 9 +msgtemplate: [nick]: [text] + +[library] +compression: off +auth_sha: on +auth_plain: on +debug: off diff --git a/therapy/ui/__init__.py b/therapy/ui/__init__.py new file mode 100644 index 0000000..0d2dd8d --- /dev/null +++ b/therapy/ui/__init__.py @@ -0,0 +1 @@ +# different user interfaces for therapy diff --git a/therapy/ui/gtk/__init__.py b/therapy/ui/gtk/__init__.py new file mode 100644 index 0000000..0d2dd8d --- /dev/null +++ b/therapy/ui/gtk/__init__.py @@ -0,0 +1 @@ +# different user interfaces for therapy diff --git a/therapy/ui/gtk/ui.py b/therapy/ui/gtk/ui.py new file mode 100644 index 0000000..b30a7b8 --- /dev/null +++ b/therapy/ui/gtk/ui.py @@ -0,0 +1,237 @@ +# $Id: ui.py,v 1.7 2003/09/23 18:04:27 an Exp $ +# $Revision + +import gc + +import os, sys, time, string +import asyncore +import gtk, pango +import ConfigParser + +main_window = None +tab_windows = [] + +nl = "\r\n" + +class Window: + + vbox = None + hbox = None + scrollwin = None + textview = None + textbuf = None + label = None + infobar = None + target = "" + view = None + + def __init__(self, target, label): + global tab_windows + + self.vbox = gtk.VBox(gtk.FALSE, 0) + self.vbox.show() + + self.hbox = gtk.HBox(gtk.FALSE, 0) + self.hbox.show() + + self.vbox.pack_start(self.hbox, gtk.FALSE, gtk.FALSE, 0) + + self.infobar = gtk.Button(target) + self.hbox.pack_start(self.infobar, gtk.TRUE, gtk.TRUE, 0) + self.infobar.show() + + self.closer = gtk.Button("close") + self.hbox.pack_start(self.closer, gtk.FALSE, gtk.FALSE, 0) + self.closer.show() + self.closer.connect("clicked", self.delete) + + self.scrollwin = gtk.ScrolledWindow() + self.scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) + + self.textview = gtk.TextView() + self.textview.set_cursor_visible(gtk.FALSE) + self.textview.set_wrap_mode(gtk.WRAP_WORD) + self.textview.set_editable(gtk.FALSE) + self.textbuf = self.textview.get_buffer() + + self.scrollwin.add(self.textview) + self.scrollwin.show() + + font = pango.FontDescription('courier Medium 12') + self.textview.modify_font(font) + self.textview.show() + + self.target = target + + if label == None: + if target.find("@") != -1: + label = target[target.rfind("@"):] + elif target.find("~") != -1: + label = target[target.rfind("~")+1:] + else: + label = target + + self.label = gtk.Label(label) + + self.vbox.pack_start(self.scrollwin, gtk.TRUE, gtk.TRUE, 0) + + main_window.notebook.append_page(self.vbox, self.label) + + tab_windows.append(self) + + def __del__(self): + pass + + def set_view(self, view): + self.view = view + + def delete(self, foo): + global tab_windows + + num = main_window.notebook.page_num(self.vbox) + main_window.notebook.remove_page(num) + self.view.delete(self.target) + tab_windows.remove(self) + + #gc.collect() + + def append_text(self, text): + try: + iter = self.textbuf.get_end_iter() + # sieht krank aus, funktioniert aber + text = text.decode("iso-8859-1").encode("utf-8") + text += nl + self.textbuf.insert(iter, text) + iter = self.textbuf.get_end_iter() + self.textbuf.place_cursor(iter) + mark = self.textbuf.get_insert() + #mark = self.textbuf.get_mark_at_iter(iter) + self.textview.scroll_mark_onscreen(mark) + + cur_page = main_window.notebook.get_current_page() + win = None + for w in tab_windows: + if main_window.notebook.page_num(w.vbox) == cur_page: + win = w + if win != self: + self.label.modify_fg(gtk.STATE_NORMAL, + self.label.get_colormap().alloc_color('darkred')) + except: + pass + +class MainWindow: + + window = None + vbox = None + notebook = None + entry = None + statusbar = None + font = None + entry_handler = None + + def __init__(self, config, entry_handler): + global main_window + + self.entry_handler = entry_handler + + # create a new window + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_size_request(350, 400) + self.window.set_title("theraPY") + self.window.connect("delete_event", gtk.mainquit) + + self.vbox = gtk.VBox(gtk.FALSE, 0) + self.window.add(self.vbox) + self.vbox.show() + + self.notebook = gtk.Notebook() + self.notebook.set_tab_pos(gtk.POS_BOTTOM) + # window.add(notebook) + self.vbox.pack_start(self.notebook, gtk.TRUE, gtk.TRUE, 0) + self.notebook.show() + + self.entry = gtk.Entry() + self.entry.set_max_length(256) + #self.entry.set_flags(gtk.HAS_DEFAULT) + self.entry.connect("activate", self.enter_callback, self.entry) + self.entry.set_text("") + # entry.insert_text(" world", len(entry.get_text())) + # entry.select_region(0, len(entry.get_text())) + self.vbox.pack_start(self.entry, gtk.FALSE, gtk.FALSE, 0) + self.font = pango.FontDescription('courier Medium 12') + self.entry.modify_font(self.font) + self.entry.show() + + self.statusbar = gtk.Statusbar() + self.vbox.pack_start(self.statusbar, gtk.FALSE, gtk.FALSE, 0) + self.statusbar.show() + + self.window.connect('key_press_event', self.key_press_callback) + self.window.show() + + main_window = self + + def key_press_callback(self, widget, event, *args): + key = event.keyval + if event.state & gtk.gdk.MOD1_MASK: + if key > 47 and key < 58: + if key == 48: + key += 10 + key -= 49 + try: + self.notebook.set_current_page(key) + except: + pass + return 1 + elif key == 110: + self.notebook.next_page() + return 1 + elif key == 112: + self.notebook.prev_page() + return 0 + + def enter_callback(self, widget, entry): + global tab_windows + + text = entry.get_text() + + cur_page = main_window.notebook.get_current_page() + target = None + for w in tab_windows: + if main_window.notebook.page_num(w.vbox) == cur_page: + target = w.target + win = w + + self.entry_handler(win, target, text) + + entry.set_text("") + +def update_notebook_tabs(): + cur_page = main_window.notebook.get_current_page() + win = None + for w in tab_windows: + if main_window.notebook.page_num(w.vbox) == cur_page: + win = w + if not win: + return + + win.label.modify_fg(gtk.STATE_NORMAL, + win.label.get_colormap().alloc_color('black')) + + if not main_window.entry.flags() & gtk.HAS_FOCUS: + i1 = win.textbuf.get_iter_at_mark(win.textbuf.get_insert()) + i2 = win.textbuf.get_iter_at_mark(win.textbuf.get_selection_bound()) + if not i1.compare(i2): + main_window.entry.grab_focus() + p = main_window.entry.get_text().__len__() + main_window.entry.select_region(p, p) + + +def poll(): + asyncore.poll(timeout = 0.0) + gtk.timeout_add(250, poll) + update_notebook_tabs() + +def mainloop(): + poll() + gtk.main() diff --git a/wx-config-example b/wx-config-example new file mode 100644 index 0000000..41f5fed --- /dev/null +++ b/wx-config-example @@ -0,0 +1,8 @@ +# this goes to ~/.pypsyc/wx-config +# or where ever you told pyPSYC it is +[gui] +wx_friend_size = 145, 291 +wx_room_size = 431, 522 +wx_friend_pos = 713, 814 +wx_room_pos = 712, 258 +