pax_global_header00006660000000000000000000000064116061767170014526gustar00rootroot0000000000000052 comment=f2c2ba399e7b72e761d92f82c172b3cba5008353 d-rats-0.3.3/000077500000000000000000000000001160617671700127235ustar00rootroot00000000000000d-rats-0.3.3/COPYING000066400000000000000000001045131160617671700137620ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . d-rats-0.3.3/MANIFEST.in000066400000000000000000000004421160617671700144610ustar00rootroot00000000000000include share/*.desktop include share/*.1.gz include forms/*.xml include forms/*.xsl include images/* include locale/*/LC_MESSAGES/D-RATS.mo include ui/*.glade include d_rats/version.py include share/d-rats2.xpm include d-rats2.ico include libexec/lzhuf include MANIFEST.in include COPYING d-rats-0.3.3/PKG-INFO000066400000000000000000000003411160617671700140160ustar00rootroot00000000000000Metadata-Version: 1.0 Name: d-rats Version: 0.3.3 Summary: D-RATS Home-page: UNKNOWN Author: Dan Smith, KK7DS Author-email: kk7ds@danplanet.com License: UNKNOWN Description: A communications tool for D-STAR Platform: UNKNOWN d-rats-0.3.3/cleanup-script.sh000077500000000000000000000003241160617671700162120ustar00rootroot00000000000000#!/bin/bash # remove the executable for lzhuf. Although source # is available, it only builds in an arcane environment, # and the license and authorship is unclear. git rm -f libexec/lzhuf rm -f libexec/lzhuf d-rats-0.3.3/d-rats000077500000000000000000000075241160617671700140530ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os from optparse import OptionParser import traceback import gtk sys.path.insert(0, os.path.join("/usr/share", "d-rats")) from d_rats import utils, spell spell.get_spell().test() IGNORE_ALL=False def handle_exception(exctyp, value, tb): global IGNORE_ALL if exctyp is KeyboardInterrupt or IGNORE_ALL: return original_excepthook(exctyp, value, tb) gtk.gdk.pointer_ungrab() gtk.gdk.keyboard_ungrab() _trace = traceback.format_exception(exctyp, value, tb) trace = os.linesep.join(_trace) print "---- GUI Exception ----\n%s\n---- End ----\n" % trace msg = """ D-RATS has encountered an error. This may be non-fatal, so you may click Ignore below to attempt to continue running. Otherwise, click 'Quit' to terminate D-RATS now. If you are planning to file a bug for this issue, please click Debug Log below and include the contents in the bug tracker. If you need to ignore all additional warnings for this session, click Ignore All. However, please reproduce and report the issue when possible. """ def extra(dialog): dialog.add_button(_("Debug Log"), gtk.RESPONSE_HELP); dialog.add_button(_("Ignore"), gtk.RESPONSE_CLOSE); dialog.add_button(_("Ignore All"), -1); dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CANCEL); dialog.set_default_response(gtk.RESPONSE_CANCEL) while True: r = utils.make_error_dialog(msg, trace, gtk.BUTTONS_NONE, gtk.MESSAGE_ERROR, extra=extra) if r == gtk.RESPONSE_CANCEL: sys.exit(1) elif r == gtk.RESPONSE_CLOSE: break elif r == -1: IGNORE_ALL=True break elif r == gtk.RESPONSE_HELP: p = platform.get_platform() p.open_text_file(p.config_file("debug.log")) def install_excepthook(): global original_excepthook original_excepthook = sys.excepthook sys.excepthook = handle_exception def ignore_exception(exctyp, value, tb): return def uninstall_excepthook(): global original_excepthook sys.excepthook = ignore_exception if __name__ == "__main__": o = OptionParser() o.add_option("-s", "--safe", dest="safe", action="store_true", help="Safe mode (ignore configuration)") o.add_option("-c", "--config", dest="config", help="Use alternate configuration directory") o.add_option("-p", "--profile", dest="profile", action="store_true", help="Enable profiling") (opts, args) = o.parse_args() from d_rats import platform if opts.config: platform.get_platform(opts.config) from d_rats import mainapp install_excepthook() import libxml2 libxml2.debugMemory(1) app = mainapp.MainApp(safe=opts.safe) if opts.profile: import cProfile cProfile.run('app.main()') else: rc = app.main() uninstall_excepthook() libxml2.dumpMemory() sys.exit(rc) d-rats-0.3.3/d-rats2.ico000066400000000000000000000172361160617671700147040ustar00rootroot00000000000000-6ˆ(-lџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџршьЯгеџџџ§§§–‘Є]J’ƒy џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџђђєўўўЌЩи;~Ѓ+n“m‹›tj“mWЋaSŠщшъџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџїїљЖЖйžсzƒВ+q˜,tœ,tœ,tœ.Z]OŽедйџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџкжш—˜Щ˜™х˜™х€‚Т(iŽ,tœ,tœ,tœ,tœHsŠїїїџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџюэђˆ~ІoYЎnlЇ˜™х”•п7]ƒ,tœ,tœ,tœ,tœ,tœ,tœˆЉџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџўўўџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ™˜Џ\J‘\J‘NC~Tc“7]‚,tœ,tœ,tœ,tœ,tœ,tœ,tœ.j‹ъъыџџџўўўџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџћћўооя€ŸД>h6^8^:bKn,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœЅБџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџўўўнніЃЄш{{Й=gBoBoBoBo@d,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœBsŽўўўџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџХПжZO{‘и˜™хotЎ@lBoBoBoBo>i$a„,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœ*o–жийџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџoЎKfBoBoBoBoBoBoCe,tœ,tœ,tœ,tœ,tœ,tœ,tœ,tœ%e‰ ЊАџџџџџџџџџџџџџџџџџџџџџџџџџџџцццССЭААШŸŸЗКЛсžЇЦm“Ќl’ЋmŒЂŽЉК™ДЦЃЛЫСЯинпп8Mi6b6[BoBoBoBoBoBoBoBo@lNn,tœ,tœ,tœ,tœ,tœ,tœ'iŽ>c†šЇжжжџџџџџџџџџџџџџџџппсЎЎФПŒг–—тxxД‰ŠЯ˜™х‹Œв=e6[9`BoBoBoBo>h@iBoBoBoBoBoBoBoBoBoBoBo?kFi%d‡,tœ,tœ,sšWy>dBoŠЊѓѓѓЮЮЮЬЬЬѓѓѓггк“”ПŒг‘’кopЈ^_Žjk —˜ф˜™х•–р}~М"@e@lBoBoBoBoBoBoBoBoBoBoBoBoBoBoBoBoBoBoBoBo=g=b Bg=cAmBoBoЊДЛџџџџџџџџџлллЗЗЛŽг˜™хef™@@`JKp‰‰Ю~ОyyЖ}~Н–—тOdšBoBoBoBo?kiBoDgўўўџџџџџџџџџџџџџџџ___ђђђџџџќќќ @dBo8_:aBoBoBoBoBo?jXt‡ђђђџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœЁЅ3[v@lFhџџџџџџџџџџџџџџџџџџ­­­aaaќќќџџџџџџ0ZwBoBoBoBoBoBoBoKl­ЗОџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЭЭЭўўўkqu !2ыыыџџџџџџџџџџџџџџџџџџћћћџџџџџџџџџџџџ(TrAm 0S6;oBC-?uFgŽžЈіііџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџтттсссяяяЯЯЯЩЩЩџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЪЮа9^Ml,Ot7ZˆBSˆVLŽfSž–ЋћћћџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџнннааадддНННеееџџџџџџџџџџџџџџџџџџџџџџџџјјј‰”œ“ Mx,tœ,tœ,tœ+q˜0W€aQœЅЁВџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџRmŸЊїїїџџџџџџџџџџџџџџџяяяЧЧЧџџџџџџђѓѓ^”,s›,tœ,tœ,tœ5U…Ћџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџb“BoHj|œееевввдддааамммџџџџџџџџџџџџћћћŽЂ­,iŒ,tœ,tœ(h‹‡–џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЅАЗAmDhЈГЛџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџщъъЌЙРІГŒЄБмннџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџўўўСХШђѓѓџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџd-rats-0.3.3/d-rats_mapdownloader000077500000000000000000000175261160617671700167720ustar00rootroot00000000000000#!/usr/bin/python import gtk import gobject import threading import time from d_rats.geopy import distance try: import mapdisplay import miscwidgets except ImportError: from d_rats import mapdisplay from d_rats import miscwidgets ZOOM_MIN = 10 ZOOM_MAX = 17 ZOOMS = {} for x in range(ZOOM_MIN, ZOOM_MAX+1): t = mapdisplay.MapTile(1, 1, x) x1, y1, x2, __ = t.tile_edges() dist = distance.distance((x1, y1), (x2, y1)).miles * 5 ZOOMS["%.1f (" %dist + _("max zoom") + " %i)" % x] = x class MapDownloader(gtk.Window): def make_info(self): text = """ This is the D-RATS map download utility. It will attempt to fetch all of the required tiles for a given center location and desired diameter. The diameter is determined by the zoom level limit. All zoom levels below the one selected are fetched. D-RATS defaults to zoom level 14, so it is recommended that you choose at least that level to fetch. This operation fetches a lot of small files and can take quite a long time.""" l = gtk.Label(text) l.set_line_wrap(True) l.show() return l def make_val(self, key, label): box = gtk.HBox(True, 2) l = gtk.Label(label) l.show() e = miscwidgets.LatLonEntry() e.show() box.pack_start(l) box.pack_start(e) box.show() self.vals[key] = e return box def make_zoom(self, key, label): min = 10 max = ZOOM_MAX default = 14 box = gtk.HBox(True, 2) l = gtk.Label(label) l.show() def byval(x, y): try: return int(float(x.split(" ")[0]) - float(y.split(" ")[0])) except Exception, e: return 0 choices = sorted(ZOOMS.keys(), cmp=byval) c = miscwidgets.make_choice(choices, False, choices[2]) c.show() box.pack_start(l) box.pack_start(c) box.show() self.vals[key] = c return box def build_bounds(self): frame = gtk.Frame("Bounds") box = gtk.VBox(True, 2) self.val_keys = { "lat" : _("Latitude"), "lon" : _("Longitude"), "zoom" : _("Diameter"), } for i in ["lat", "lon"]: box.pack_start(self.make_val(i, self.val_keys[i]), 0,0,0) box.pack_start(self.make_zoom("zoom", _("Diameter") + " (miles)"), 0,0,0) box.show() frame.add(box) frame.show() return frame def _update(self, prog, status): if prog: self.progbar.set_fraction(prog) self.status.set_text(status) if not self.enabled: self.thread.join() self.stop_button.set_sensitive(False) self.start_button.set_sensitive(True) def update(self, prog, status): gobject.idle_add(self._update, prog, status) def download_zoom(self, tick, zoom, x, y): if zoom == ZOOM_MAX + 1: return nw = (2 * x, 2 * y) sw = (2 * x, (2 * y) + 1) ne = ((2 * x) + 1, 2 * y) se = ((2 * x) + 1, (2 * y) + 1) for i in (nw, sw, ne, se): if self.enabled: self.download_zoom(tick, zoom + 1, *i) print "Downloading %i:%i,%i" % (zoom, x, y) t = mapdisplay.MapTile(0, 0, zoom) t.x = x t.y = y t.fetch() tick() def download_thread(self, **vals): self.complete = False self.enabled = True zoom = vals["zoom"] lat = vals["lat"] lon = vals["lon"] center = mapdisplay.MapTile(lat, lon, zoom) self.count = 0 # The number of tiles $levels below zoom is 4^($levels) # Each zoom level has a tile as well, so there are # 4^$level under each zoom level, not counting the last depth = pow(4, ZOOM_MAX - zoom) extra = 0 for i in range(0, ZOOM_MAX - zoom): extra += pow(4, i) self.max = (depth + extra) * 25 def status_tick(): self.count += 1 percent = (float(self.count) / self.max) self.update(percent, _("Downloading") + " (%.0f %%)" % (percent * 100.0)) for i in range(-2, 3): if not self.enabled: break for j in range(-2, 3): if not self.enabled: break tile = center + (i, j) self.download_zoom(status_tick, zoom, tile.x, tile.y) if self.enabled: self.update(1.0, _("Complete")) else: self.update(None, _("Stopped")) self.complete = True self.enabled = False def show_field_error(self, field): d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) d.set_property("text", _("Invalid value for") + " `%s'" % field) d.run() d.destroy() def show_range_error(self, field): d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) d.set_property("text", _("Invalid range for") + " %s" % field) d.run() d.destroy() def do_start(self, widget, data=None): vals = {} for k,e in self.vals.items(): try: if "zoom" in k: dia = e.get_active_text() vals[k] = ZOOMS[dia] else: vals[k] = e.value() except ValueError, e: self.show_field_error(self.val_keys[k]) return print "Zoom is %i" % vals["zoom"] self.start_button.set_sensitive(False) self.stop_button.set_sensitive(True) print "Starting" self.thread = threading.Thread(target=self.download_thread, kwargs=vals) self.thread.setDaemon(True) self.thread.start() print "Started" def do_stop(self, widget, data=None): self.start_button.set_sensitive(True) self.stop_button.set_sensitive(False) self.enabled = False self.thread.join() def make_control_buttons(self): box = gtk.HBox(True, 2) self.start_button = gtk.Button(_("Start")) self.start_button.set_size_request(75, 30) self.start_button.connect("clicked", self.do_start) self.start_button.show() self.stop_button = gtk.Button(_("Stop")) self.stop_button.set_size_request(75, 30) self.stop_button.set_sensitive(False) self.stop_button.connect("clicked", self.do_stop) self.stop_button.show() box.pack_start(self.start_button, 0,0,0) box.pack_start(self.stop_button, 0,0,0) box.show() return box def build_controls(self): frame = gtk.Frame(_("Controls")) box = gtk.VBox(False, 2) self.progbar = gtk.ProgressBar() self.progbar.show() self.status = gtk.Label("") self.status.show() box.pack_start(self.progbar, 0,0,0) box.pack_start(self.status, 0,0,0) box.pack_start(self.make_control_buttons(), 0,0,0) box.show() frame.add(box) frame.show() return frame def build_gui(self): box = gtk.VBox(False, 2) box.pack_start(self.make_info()) box.pack_start(self.build_bounds()) box.pack_start(self.build_controls()) box.show() return box def __init__(self, title=_("Map Download Utility")): gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_title(title) self.vals = {} self.enabled = False self.thread = None self.completed = False self.add(self.build_gui()) if __name__=="__main__": gobject.threads_init() def stop(*args): gtk.main_quit() w = MapDownloader() w.connect("destroy", stop) w.connect("delete_event", stop) w.show() gtk.main() d-rats-0.3.3/d-rats_repeater000077500000000000000000000650751160617671700157470ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading import time import socket import ConfigParser import os import gettext gettext.install("D-RATS") from d_rats import platform from d_rats import transport from d_rats import comm if __name__ == "__main__": from optparse import OptionParser o = OptionParser() o.add_option("-c", "--config", dest="config", help="Use alternate configuration directory") o.add_option("-d", "--debug", dest="debug", action="store_true", help="Show debug messages on stdout") o.add_option("-C", "--console", dest="console", action="store_true", help="Run in console mode only") (opts, args) = o.parse_args() if opts.config: platform.get_platform(opts.config) from d_rats.comm import SWFSerial from d_rats import utils def call_with_lock(lock, fn, *args): lock.acquire() r = fn(*args) lock.release() return r IN = 0 OUT = 1 PEEK = 2 def DEBUG(str): pass #print str class CallInfo: def __init__(self, call, transport): self.__call = call self.just_heard(transport) def get_call(self): return self.__call def just_heard(self, transport): self.__heard = time.time() self.__transport = transport def last_heard(self): return time.time() - self.__heard def last_transport(self): return self.__transport def call_in_list(callinfo, call): for info in callinfo: if call == info.get_call(): return True return False class Repeater: def __init__(self, id="D-RATS Network Proxy", reqauth=False, trustlocal=False, gps_okay_ports=[]): self.paths = [] self.calls = {} self.thread = None self.enabled = True self.socket = None self.repeat_thread = None self.id = id self.reqauth = reqauth self.trustlocal = trustlocal self.condition = threading.Condition() self.gps_socket = None self.gps_sockets = [] self.gps_okay_ports = gps_okay_ports # Forget port for a station after 10 minutes self.__call_timeout = 600 def __should_repeat_gps(self, transport, frame): if not self.gps_okay_ports: return True else: return transport.name in self.gps_okay_ports def __repeat(self, transport, frame): if frame.d_station == "!": return if frame.s_station == frame.d_station == "CQCQCQ" and \ frame.session == 1 and \ frame.data.startswith("$") and \ self.__should_repeat_gps(transport, frame): for s in self.gps_sockets: s.send(frame.data) srcinfo = self.calls.get(frame.s_station, None) if srcinfo is None and frame.s_station != "CQCQCQ": print "Adding new station %s to port %s" % (frame.s_station, transport) self.calls[frame.s_station] = CallInfo(frame.s_station, transport) elif srcinfo: if srcinfo.last_transport() != transport: print "Station %s moved to port %s" % (frame.s_station, transport) srcinfo.just_heard(transport) dstinfo = self.calls.get(frame.d_station, None) if dstinfo is not None: if not dstinfo.last_transport().enabled: print "Last transport for %s is dead" % frame.d_station elif dstinfo.last_heard() < self.__call_timeout: print "Delivering frame to %s at %s" % \ (frame.d_station, dstinfo.last_transport()) dstinfo.last_transport().send_frame(frame.get_copy()) return else: print "Last port for %s was %i sec ago (>%i sec)" % \ (frame.d_station, dstinfo.last_heard(), self.__call_timeout) print "Repeating frame to %s on all ports" % frame.d_station for path in self.paths[:]: if path == transport: continue if not path.enabled: print "Found a stale path, removing..." path.disable() self.paths.remove(path) else: path.send_frame(frame.get_copy()) def add_new_transport(self, transport): self.paths.append(transport) def handler(frame): self.condition.acquire() try: self.__repeat(transport, frame) except Exception, e: print "Exception during __repeat: %s" % e self.condition.release() transport.inhandler = handler def auth_exchange(self, pipe): username = password = None count = 0 def readline(_s): data = "" while "\r\n" not in data: try: _d = _s.read(32) except socket.timeout: continue if _d == "": break data += _d return data.strip() while (not username or not password) and count < 3: line = readline(pipe) if not line: continue try: cmd, value = line.split(" ", 1) except Exception, e: print "Unable to read auth command: `%s': %s" % (line, e) pipe.write("501 Invalid Syntax\r\n") break cmd = cmd.upper() if cmd == "USER" and not username and not password: username = value elif cmd == "PASS" and username and not password: password = value else: pipe.write("201 Protocol violation\r\n") break if username and not password: pipe.write("102 %s okay\r\n" % cmd) if not username or not password: print "Negotiation failed with client" return username, password def auth_user(self, pipe): host, port = pipe._socket.getpeername() if not self.reqauth: pipe.write("100 Authentication not required\r\n") return True elif self.trustlocal and host == "127.0.0.1": pipe.write("100 Authentication not required for localhost\r\n") return True auth_fn = platform.get_platform().config_file("users.txt") try: auth = file(auth_fn) lines = auth.readlines() auth.close() except Exception, e: print "Failed to open %s: %s" % (auth_fn, e) pipe.write("101 Authorization required\r\n") username, password = self.auth_exchange(pipe) lno = 1 for line in lines: line = line.strip() try: u, p = line.split(" ", 1) u = u.upper() except Exception, e: print "Failed to parse line %i in users.txt: %s" % (lno, line) continue if u == username and p == password: print "Authorized user %s" % u pipe.write("200 Authorized\r\n") return True print "User %s failed to authenticate" % username pipe.write("500 Not authorized\r\n") return False def accept_new(self): if not self.socket: return try: (csocket, addr) = self.socket.accept() except: return print "Accepted new client %s:%i" % addr path = comm.SocketDataPath(csocket) tport = transport.Transporter(path, authfn=self.auth_user, warmup_timeout=0) self.add_new_transport(tport) def accept_new_gps(self): if not self.gps_socket: return try: (csocket, addr) = self.gps_socket.accept() except: return print "Accepted new GPS client %s:%i" % addr self.gps_sockets.append(csocket) def listen_on(self, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setblocking(0) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('0.0.0.0', port)) s.listen(0) return s def _repeat(self): while self.enabled: self.condition.acquire() self.accept_new() self.accept_new_gps() self.condition.release() time.sleep(0.5) print "Repeater thread ended" def repeat(self): self.repeat_thread = threading.Thread(target=self._repeat) self.repeat_thread.setDaemon(True) self.repeat_thread.start() def stop(self): self.enabled = False self.condition.acquire() self.condition.notify() self.condition.release() if self.repeat_thread: print "Stopping repeater" self.repeat_thread.join() for p in self.paths: print "Stopping" p.disable() if self.socket: self.socket.close() class RepeaterUI: def __init__(self): self.repeater = None self.tap = None self.tick = 0 self.platform = platform.get_platform() self.config = self.load_config() def load_config(self): self.config_fn = self.platform.config_file("repeater.config") config = ConfigParser.ConfigParser() config.add_section("settings") config.set("settings", "devices", "[]") config.set("settings", "acceptnet", "True") config.set("settings", "netport", "9000") config.set("settings", "id", "W1AW") config.set("settings", "idfreq", "30") config.set("settings", "require_auth", "False") config.set("settings", "trust_local", "True") config.set("settings", "gpsport", "9500") config.add_section("tweaks") config.set("tweaks", "allow_gps", "") config.read(self.config_fn) return config def save_config(self, config): self.sync_config() f = file(self.config_fn, "w") config.write(f) f.close() def add_outgoing_paths(self, id, paths): reqauth = self.config.get("settings", "require_auth") == "True" trustlocal = self.config.get("settings", "trust_local") == "True" gps_okay_ports = self.config.get("tweaks", "allow_gps").split(",") print "Repeater id is %s" % id self.repeater = Repeater(id, reqauth, trustlocal, gps_okay_ports) for dev,param in paths: to = 0 if dev.startswith("net:"): try: net, host, port = dev.split(":", 2) port = int(port) except Exception, e: print "Invalid net string: %s (%s)" % (dev, e) continue print "Socket %s %i (%s)" % (host, port, param) if param: path = comm.SocketDataPath((host, port, id, param)) else: path = comm.SocketDataPath((host, port)) elif dev.startswith("tnc:"): try: tnc, port, device = dev.split(":", 2) device = int(device) except Exception, e: print "Invalid tnc string: %s (%s)" % (dev, e) continue print "TNC %s %i" % (dev.replace("tnc:", ""), int(param)) path = comm.TNCDataPath((dev.replace("tnc:", ""), int(param))) else: print "Serial: %s %i" % (dev, int(param)) path = comm.SerialDataPath((dev, int(param))) to = 3 path.connect() tport = transport.Transporter(path, warmup_timout=to, name=dev) self.repeater.add_new_transport(tport) class RepeaterGUI(RepeaterUI): def add_serial(self, widget): name, portspec, param = prompt_for_port(None, pname=False) if portspec is None: return self.dev_list.add_item(portspec, param) def sig_destroy(self, widget, data=None): self.button_off(None, False) self.save_config(self.config) gtk.main_quit() def ev_delete(self, widget, event, data=None): self.button_off(None, False) self.save_config(self.config) self.repeater.stop() gtk.main_quit() def make_side_buttons(self): vbox = gtk.VBox(False, 2) but_add = gtk.Button("Add") but_add.connect("clicked", self.add_serial) but_add.set_size_request(75, 30) but_add.show() vbox.pack_start(but_add, 0,0,0) but_remove = gtk.Button("Remove") but_remove.set_size_request(75, 30) but_remove.connect("clicked", self.button_remove) but_remove.show() vbox.pack_start(but_remove, 0,0,0) vbox.show() return vbox def load_devices(self): try: l = eval(self.config.get("settings", "devices")) for d,r in l: self.dev_list.add_item(d, r) except Exception, e: print "Unable to load devices: %s" % e def make_devices(self): frame = gtk.Frame("Paths") vbox = gtk.VBox(False, 2) frame.add(vbox) hbox = gtk.HBox(False, 2) self.dev_list = miscwidgets.ListWidget([(gobject.TYPE_STRING, "Device"), (gobject.TYPE_STRING, "Param")]) self.dev_list.show() self.load_devices() sw = gtk.ScrolledWindow() sw.add_with_viewport(self.dev_list) sw.show() hbox.pack_start(sw, 1,1,1) hbox.pack_start(self.make_side_buttons(), 0,0,0) hbox.show() vbox.pack_start(hbox, 1,1,1) vbox.show() frame.show() return frame def make_network(self): frame = gtk.Frame("Network") vbox = gtk.VBox(False, 2) frame.add(vbox) hbox = gtk.HBox(False, 2) self.net_enabled = gtk.CheckButton("Accept incoming connections") try: accept = self.config.getboolean("settings", "acceptnet") except: accept = True self.net_enabled.set_active(accept) self.net_enabled.show() hbox.pack_start(self.net_enabled, 0,0,0) self.entry_port = gtk.Entry() try: port = self.config.get("settings", "netport") except: port = "9000" self.entry_gpsport = gtk.Entry() try: gpsport = self.config.get("settings", "gpsport") except : port = "9500" self.entry_gpsport.set_text(gpsport) self.entry_gpsport.set_size_request(100, -1) self.entry_gpsport.show() hbox.pack_end(self.entry_gpsport, 0,0,0) lab = gtk.Label("GPS Port:") lab.show() hbox.pack_end(lab, 0,0,0) self.entry_port.set_text(port) self.entry_port.set_size_request(100, -1) self.entry_port.show() hbox.pack_end(self.entry_port, 0,0,0) lab = gtk.Label("Port:") lab.show() hbox.pack_end(lab, 0,0,0) hbox.show() vbox.pack_start(hbox, 0,0,0) vbox.show() frame.show() return frame def make_bottom_buttons(self): hbox = gtk.HBox(False, 2) self.but_on = gtk.Button("On") self.but_on.set_size_request(75, 30) self.but_on.connect("clicked", self.button_on) self.but_on.show() hbox.pack_start(self.but_on, 0,0,0) self.but_off = gtk.Button("Off") self.but_off.set_size_request(75, 30) self.but_off.connect("clicked", self.button_off) self.but_off.set_sensitive(False) self.but_off.show() hbox.pack_start(self.but_off, 0,0,0) hbox.show() return hbox def make_id(self): frame = gtk.Frame("Repeater Callsign") hbox = gtk.HBox(False, 2) self.entry_id = gtk.Entry() try: deftxt = self.config.get("settings", "id") except: deftxt = "W1AW" self.entry_id.set_text(deftxt) self.entry_id.set_max_length(8) self.entry_id.show() hbox.pack_start(self.entry_id, 1,1,1) try: idfreq = self.config.get("settings", "idfreq") except: idfreq = "30" self.id_freq = make_choice(["Never", "30", "60", "120"], True, idfreq) self.id_freq.set_size_request(75, -1) #self.id_freq.show() hbox.pack_start(self.id_freq, 0,0,0) hbox.show() frame.add(hbox) frame.show() return frame def make_auth(self): frame = gtk.Frame("Authentication") hbox = gtk.HBox(False, 20) def toggle_option(cb, option): self.config.set("settings", option, str(cb.get_active())) self.req_auth = gtk.CheckButton("Require Authentication") self.req_auth.connect("toggled", toggle_option, "require_auth") self.req_auth.show() self.req_auth.set_active(self.config.getboolean("settings", "require_auth")) hbox.pack_start(self.req_auth, 0, 0, 0) self.trust_local = gtk.CheckButton("Trust localhost") self.trust_local.connect("toggled", toggle_option, "trust_local") self.trust_local.show() self.trust_local.set_active(self.config.getboolean("settings", "trust_local")) hbox.pack_start(self.trust_local, 0, 0, 0) def do_edit_users(but): p = platform.get_platform() p.open_text_file(p.config_file("users.txt")) edit_users = gtk.Button("Edit Users") edit_users.connect("clicked", do_edit_users) edit_users.show() edit_users.set_size_request(75, 30) hbox.pack_end(edit_users, 0, 0, 0) hbox.show() frame.add(hbox) frame.show() return frame def make_settings(self): vbox = gtk.VBox(False, 5) hbox = gtk.HBox(False, 5) vbox.pack_start(self.make_devices(), 1,1,1) vbox.pack_start(self.make_network(), 0,0,0) vbox.pack_start(self.make_auth(), 0,0,0) vbox.pack_start(self.make_id(), 0,0,0) vbox.pack_start(hbox, 0, 0, 0) hbox.show() vbox.show() self.settings = vbox return vbox def make_connected(self): frame = gtk.Frame("Connected Paths") idlist = miscwidgets.ListWidget([(gobject.TYPE_STRING, "ID")]) idlist.show() self.conn_list = gtk.ScrolledWindow() self.conn_list.add_with_viewport(idlist) self.conn_list.show() frame.add(self.conn_list) frame.show() return frame def make_traffic(self): frame = gtk.Frame("Traffic Monitor") self.traffic_buffer = gtk.TextBuffer() self.traffic_view = gtk.TextView(buffer=self.traffic_buffer) self.traffic_view.set_wrap_mode(gtk.WRAP_WORD) self.traffic_view.show() self.traffic_buffer.create_mark("end", self.traffic_buffer.get_end_iter(), False) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add(self.traffic_view) sw.show() frame.add(sw) frame.show() return frame def make_monitor(self): vbox = gtk.VBox(False, 5) vbox.pack_start(self.make_connected(), 1,1,1) vbox.pack_start(self.make_traffic(), 1,1,1) vbox.show() return vbox def sync_config(self): id = self.entry_id.get_text() #idfreq = self.id_freq.get_active_text() port = self.entry_port.get_text() acceptnet = str(self.net_enabled.get_active()) devices = self.dev_list.get_values() auth = self.req_auth.get_active() local = self.trust_local.get_active() gpsport = self.entry_gpsport.get_text() self.config.set("settings", "id", id) #self.config.set("settings", "idfreq", idfreq) self.config.set("settings", "netport", port) self.config.set("settings", "acceptnet", acceptnet) self.config.set("settings", "devices", devices) self.config.set("settings", "require_auth", str(auth)) self.config.set("settings", "trust_local", str(local)) self.config.set("settings", "gpsport", gpsport) def button_remove(self, widget): self.dev_list.remove_selected() def button_on(self, widget, data=None): self.tick = 0 self.config.set("settings", "state", "True") self.save_config(self.config) self.but_off.set_sensitive(True) self.but_on.set_sensitive(False) self.settings.set_sensitive(False) self.add_outgoing_paths(self.config.get("settings", "id"), self.dev_list.get_values()) try: port = int(self.entry_port.get_text()) gpsport = int(self.entry_gpsport.get_text()) enabled = self.net_enabled.get_active() except: port = 0 gpsport = 0 if port and enabled: self.repeater.socket = self.repeater.listen_on(port) self.repeater.gps_socket = self.repeater.listen_on(gpsport) #self.tap = LoopDataPath("TAP", self.repeater.condition) #self.repeater.paths.append(self.tap) self.repeater.repeat() def button_off(self, widget, user=True): if user: self.config.set("settings", "state", "False") self.save_config(self.config) self.but_off.set_sensitive(False) self.but_on.set_sensitive(True) self.settings.set_sensitive(True) if self.repeater: self.repeater.stop() self.repeater = None self.tap = None def update(self): if self.repeater: paths = self.repeater.paths l = [(x.id,) for x in paths] else: l = [] if ("TAP",) in l: l.remove(("TAP",)) self.conn_list.child.set_values(l) if self.tap: traffic = self.tap.peek() end = self.traffic_buffer.get_end_iter() self.traffic_buffer.insert(end, utils.filter_to_ascii(traffic)) count = self.traffic_buffer.get_line_count() if count > 200: start = self.traffic_buffer.get_start_iter() limit = self.traffic_buffer.get_iter_at_line(count - 200) self.traffic_buffer.delete(start, limit) endmark = self.traffic_buffer.get_mark("end") self.traffic_view.scroll_to_mark(endmark, 0.0, True, 0, 1) try: limit = int(self.id_freq.get_active_text()) if (self.tick / 60) == limit: self.repeater.send_data(None, self.entry_id.get_text()) self.tick = 0 except: pass self.tick += 1 return True def __init__(self): RepeaterUI.__init__(self) self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_default_size(450, 380) self.window.connect("delete_event", self.ev_delete) self.window.connect("destroy", self.sig_destroy) self.window.set_title("D-RATS Repeater Proxy") vbox = gtk.VBox(False, 5) self.tabs = gtk.Notebook() self.tabs.append_page(self.make_settings(), gtk.Label("Settings")) # FIXME: later # self.tabs.append_page(self.make_monitor(), gtk.Label("Monitor")) self.tabs.show() vbox.pack_start(self.tabs, 1,1,1) vbox.pack_start(self.make_bottom_buttons(), 0,0,0) vbox.show() self.window.add(vbox) self.window.show() #gobject.timeout_add(1000, self.update) try: if self.config.get("settings", "state") == "True": self.button_on(None, None) except Exception, e: print e class RepeaterConsole(RepeaterUI): def main(self): devices = eval(self.config.get("settings", "devices")) self.add_outgoing_paths(self.config.get("settings", "id"), devices) try: acceptnet = eval(self.config.get("settings", "acceptnet")) netport = int(self.config.get("settings", "netport")) gpsport = int(self.config.get("settings", "gpsport")) idfreq = self.config.get("settings", "idfreq") if idfreq == "Never": idfreq = 0 else: idfreq = int(idfreq) except Exception, e: print "Failed to parse network info: %s" % e acceptnet = False if acceptnet: self.repeater.socket = self.repeater.listen_on(netport) self.repeater.gps_socket = self.repeater.listen_on(gpsport) self.repeater.repeat() while True: try: time.sleep(0.25) except KeyboardInterrupt: self.repeater.stop() break if __name__=="__main__": import sys if not opts.debug: p = platform.get_platform() f = file(p.config_file("repeater.log"), "w", 0) if f: sys.stdout = f sys.stderr = f else: print "Failed to open log" if opts.console: r = RepeaterConsole() r.main() else: import gtk import gobject from d_rats.miscwidgets import make_choice from d_rats import miscwidgets from d_rats.config import prompt_for_port gobject.threads_init() g = RepeaterGUI() gtk.main() d-rats-0.3.3/d_rats/000077500000000000000000000000001160617671700141775ustar00rootroot00000000000000d-rats-0.3.3/d_rats/__init__.py000066400000000000000000000000071160617671700163050ustar00rootroot00000000000000# Foo d-rats-0.3.3/d_rats/agw.py000066400000000000000000000214001160617671700153240ustar00rootroot00000000000000import struct import utils import sys import socket import threading class AGWFrame: kind = 0 def __init__(self): self.port = 0 self.res1 = self.res2 = self.res3 = 0 self.res4 = self.res5 = self.res6 = 0 self.pid = 0; self.call_from = "".ljust(10) self.call_to = "".ljust(10) self.len = 0 self.payload = "" def packed(self): p = struct.pack("BBBBBBBB10s10sII", self.port, self.res1, self.res2, self.res3, self.kind, self.res4, self.pid, self.res5, self.call_from, self.call_to, self.len, self.res6); return p + self.payload; def unpack(self, data): self.port,\ self.res1, self.res2, self.res3, \ self.kind, \ self.res4, \ self.pid, \ self.res5, \ self.call_from, self.call_to, \ self.len, \ self.res6 = struct.unpack("BBBBBBBB10s10sII", data[:36]); self.payload = data[36:] if len(self.payload) != self.len: raise Exception("Expecting payload of %i, got %i" % \ (self.len, len(self.payload))) def set_payload(self, data): self.payload = data self.len = len(self.payload) def get_payload(self): return self.payload def set_from(self, call): self.call_from = call[:9].ljust(9, '\0') + '\0' def get_from(self): return self.call_from def set_to(self, call): self.call_to = call[:10].ljust(9, '\0') + '\0' def get_to(self): return self.call_to def __str__(self): return "%s -> %s [%s]: %s" % (self.call_from, self.call_to, chr(self.kind), utils.filter_to_ascii(self.payload)) class AGWFrame_k(AGWFrame): kind = ord("k") class AGWFrame_K(AGWFrame): kind = ord('K') class AGWFrame_C(AGWFrame): kind = ord('C') class AGWFrame_d(AGWFrame): kind = ord('d') class AGWFrame_D(AGWFrame): kind = ord('D') class AGWFrame_X(AGWFrame): kind = ord('X') class AGWFrame_x(AGWFrame): kind = ord('x') AGW_FRAMES = { "k" : AGWFrame_k, "K" : AGWFrame_K, "C" : AGWFrame_C, "d" : AGWFrame_d, "D" : AGWFrame_D, "X" : AGWFrame_X, "x" : AGWFrame_x, } class AGWConnection: def __init__(self, addr, port, timeout=0): self.__lock = threading.Lock() self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if timeout: self._s.settimeout(timeout) self._s.connect((addr, port)) self._buf = "" self._framebuf = {} for i in AGW_FRAMES.keys(): self._framebuf[ord(i)] = [] def _detect_frame(self, data): kind = data[4] return AGW_FRAMES[kind]() def send_frame(self, f): self._s.send(f.packed()) def __recv_frame(self): try: c = self._s.recv(1) except socket.timeout: return None if len(c) == 0: # Socket closed self.close() self._buf += c if len(self._buf) >= 36: f = self._detect_frame(self._buf) try: f.unpack(self._buf) self._buf = "" except Exception, e: return None return f return None def recv_frame_type(self, kind, poll=False): while True: buffered = self._framebuf.get(ord(kind), []) if buffered: return buffered.pop() self.__lock.acquire() f = self.__recv_frame() self.__lock.release() if f: print "Got %s frame while waiting for %s" % (chr(f.kind), kind) self._framebuf[f.kind].insert(0, f) elif not poll: return None def close(self): self._s.close() def enable_raw(self): kf = AGWFrame_k() self.send_frame(kf) class AGW_AX25_Connection: def __init__(self, agw, mycall): self._agw = agw self._mycall = mycall self._inbuf = "" xf = AGWFrame_X() xf.set_from(mycall) self._agw.send_frame(xf) f = self._agw.recv_frame_type("X", True) def connect(self, tocall): cf = AGWFrame_C() cf.set_from(self._mycall) cf.set_to(tocall) self._agw.send_frame(cf) f = self._agw.recv_frame_type("C", True) print f.get_payload() def disconnect(self): df = AGWFrame_d() df.set_from(self._mycall) self._agw.send_frame(df) f = self._agw.recv_frame_type("d", True) print f.get_payload() def send(self, data): df = AGWFrame_D() df.set_payload(data) self._agw.send_frame(df) def recv(self, length=0): def consume(count): b = self._inbuf[:count] self._inbuf = self._inbuf[count:] return b if length and length < len(self._inbuf): return consume(length) f = self._agw.recv_frame_type("D") if f: self._inbuf += f.get_payload() if not length: return consume(len(self._inbuf)) else: return consume(length) def recv_text(self): return self.recv().replace("\r", "\n") def agw_recv_frame(s): data = "" while True: data += s.recv(1) if len(data) >= 36: f = AGWFrame_K() try: f.unpack(data) data = "" except Exception, e: #print "Failed: %s" % e continue print "%s -> %s [%s]" % (f.get_from(), f.get_to(), chr(f.kind)) utils.hexprint(f.get_payload()) return def test_raw_recv(s): f = AGWFrame_k() s.send(f.packed()) while True: agw_recv_frame(s) def test_connect(s): xf = AGWFrame_X() xf.set_from("KK7DS") s.send(xf.packed()) agw_recv_frame(s) cf = AGWFrame_C() cf.set_from("KK7DS") cf.set_to("PSVNOD") s.send(cf.packed()) agw_recv_frame(s) while True: agw_recv_frame(s) def test_class_connect(): agw = AGWConnection("127.0.0.1", 8000, 0.5) axc = AGW_AX25_Connection(agw, "KK7DS") axc.connect("N7AAM-11") print axc.recv_text() while True: print "packet> ", l = sys.stdin.readline().strip() if len(l) > 0: axc.send(l + "\r") r = True while r: r = axc.recv_text() print r axc.disconnect() def ssid(call): if "-" in call: try: c, s = call.split("-", 1) except Exception, e: raise Exception("Callsign `%s' not in CCCCCC-N format" % call) else: c = call s = 0 if len(c) > 6: raise Exception("Callsign `%s' is too long" % c) c = c.ljust(6) try: s = int(s) except Exception, e: raise Exception("Invalid SSID `%s'" % s) if s < 0 or s > 7: raise Exception("Invalid SSID `%i'" % s) return c, s def encode_ssid(s, last=False): if last: l = 0x61 else: l = 0x60 return chr((s << 1) | l) # conn is the AGWConnection # dcall is the destination # spath is a list of either the source, or source + digis # data is the data to transmit def transmit_data(conn, dcall, spath, data): c, s = ssid(dcall) # Encode the call by grabbing each character and shifting # left one bit dst = "".join([chr(ord(x) << 1) for x in c]) dst += encode_ssid(s) src = "" for scall in spath: c, s = ssid(scall) src += "".join([chr(ord(x) << 1) for x in c]) src += encode_ssid(s, spath[-1] == scall) d = struct.pack("B7s%isBB" % len(src), 0x00, # Space for flag (?) dst, # Dest Call src, # Source Path 0x3E, # Info 0xF0) # PID: No layer 3 d += data utils.hexprint(d) f = AGWFrame_K() f.set_payload(d) conn.send_frame(f) def receive_data(conn, blocking=False): f = conn.recv_frame_type("K", blocking) if f: return f.get_payload() else: return "" def test(conn): f = AGWFrame_K() conn.send_frame(f) if __name__ == "__main__": #s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #s.connect(("127.0.0.1", 8000)) #test_raw_recv(s) #test_connect(s) agw = AGWConnection("127.0.0.1", 8000, 0.5) agw.enable_raw() #test_class_connect() #test_ui() transmit_data(agw, "CQ", ["KK7DS", "KK7DS-3"], "foo") d-rats-0.3.3/d_rats/ax25.py000066400000000000000000000024621160617671700153340ustar00rootroot00000000000000bstr_pos = lambda n: n>0 and bstr_pos(n>>1)+str(n&1) or '' class BitStuffContext: def __init__(self): self.outbound = "" self.register = 0 self.bits = 0 self.ones = 0 def push(self): self.outbound += chr(self.register) self.register = self.bits = self.ones = 0 def _store_bit(self, bit): self.register <<= 1 if bit: self.register |= 0x01 self.ones += 1 else: self.ones = 0 print "Register: %s" % bstr_pos(self.register) self.bits += 1 if self.bits == 8: print "Pushing" self.push() def store_bit(self, bit): if bit and self.ones == 5: print "Stuffing!" self._store_bit(0) self._store_bit(bit) def get_output(self): if self.bits: for i in range(self.bits, 8): self.store_bit(0) return self.outbound def bitstuff(data): ctx = BitStuffContext() for byte in data: for bit in range(0,8): ctx.store_bit(ord(byte) & (1 << bit)) return ctx.get_output() if __name__ == "__main__": from d_rats.utils import hexprint data = "\xFF\xFF\xFF" print "Start:" hexprint(data) print "\nStuffed:" hexprint(bitstuff(data)) d-rats-0.3.3/d_rats/callsigns.py000066400000000000000000000015651160617671700165370ustar00rootroot00000000000000import re def find_us_callsigns(string): extra2x1 = "[aAkKnNwW][A-z][0-9][A-z]" others = "[aAkKnNwW][A-z]?[0-9][A-z]{2,3}" regex = "\\b(%s|%s)\\b" % (extra2x1, others) return re.findall(regex, string) def find_au_callsigns(string): regex = '\\b[Vv][Kk][0-9][Ff]?[A-z]{2,3}' return re.findall(regex, string) def find_ca_callsigns(string): regex = '[Vv][EeAa][0-9][A-z]{2,3}' return re.findall(regex, string) callsign_functions = { "US" : find_us_callsigns, "Australia" : find_au_callsigns, "Canada" : find_ca_callsigns, } def find_callsigns(config, string): list = [] cs = eval(config.get("prefs", "callsigns")) enabled = [y for x,y in cs if x] for t in callsign_functions.keys(): if callsign_functions.has_key(t) and t in enabled: list += callsign_functions[t](string) return list d-rats-0.3.3/d_rats/cap.py000066400000000000000000000105341160617671700153170ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import libxml2 import urllib import tempfile import datetime try: from hashlib import md5 except ImportError: print "Installing hashlib replacement hack" from utils import ExternalHash as md5 def ev_cmp_exp(ev1, ev2): if ev1.expires < ev2.expires: return -1 else: return 1 def ev_cmp_eff(ev1, ev2): if ev1.effective < ev2.effective: return -1 else: return 1 FMT = "%Y-%m-%dT%H:%M:%S" class CAPEvent(object): def __init__(self): self.category = None self.event = None self.urgency = None self.severity = None self.certainty = None self.effective = None self.expires = None self.headline = None self.description = None # def __setattr__(self, name, val): # if not hasattr(self, name): # raise ValueError("No such attribute `%s'" % name) # # self.__dict__[name] = val def from_libxml_node(self, infonode): child = infonode.children while child: content = child.getContent().strip() if child.name in ["effective", "expires"]: content = datetime.datetime.strptime(content, "%Y-%m-%dT%H:%M:%S") if child.name in self.__dict__.keys(): self.__dict__[child.name] = content child = child.next def __str__(self): return "%s (%s): %s..." % (self.headline, self.expires.strftime(FMT), self.description[:20]) def report(self): return """ %s (%s - %s) %s """ % (self.headline, self.effective.strftime(FMT), self.expires.strftime(FMT), self.description) class CAPParser(object): def __init__(self, filename): doc = libxml2.parseFile(filename) root = doc.children self.events = [] hashes = [] child = root.children while child: if child.name == "info": try: ev = CAPEvent() ev.from_libxml_node(child) hash = md5() hash.update(ev.description) if hash.digest() not in hashes: self.events.append(ev) hashes.append(hash.digest()) except Exception, e: print "Unable to parse CAP node: %s (%s)" % (child.name, e) child = child.next self.events.sort(ev_cmp_eff) def expired_events(self): return sorted([x for x in self.events if x.expires < datetime.datetime.now()], ev_cmp_eff) def unexpired_events(self): return sorted([x for x in self.events if x.expires > datetime.datetime.now()], ev_cmp_eff) def events_expiring_after(self, date): return sorted([x for x in self.events if x.expires > date], ev_cmp_eff) def events_effective_after(self, date): return sorted([x for x in self.events if x.effective > date], ev_cmp_eff) class CAPParserURL(CAPParser): def __init__(self, url): tmpf = tempfile.NamedTemporaryFile() name = tmpf.name tmpf.close() urllib.urlretrieve(url, name) CAPParser.__init__(self, name) if __name__ == "__main__": import sys #cp = CAPParser(sys.argv[1]) cp = CAPParserURL("http://www.weather.gov/alerts/fl.cap") epoch = datetime.datetime(2008, 9, 29, 00, 59, 00) c = 0 for i in cp.events_expiring_after(epoch): print i.report() c += 1 print "%i events" % c d-rats-0.3.3/d_rats/comm.py000066400000000000000000000446761160617671700155250ustar00rootroot00000000000000import serial import socket import time import struct import select import utils import agw class DataPathError(Exception): pass class DataPathNotConnectedError(DataPathError): pass class DataPathIOError(DataPathError): pass ASCII_XON = chr(17) ASCII_XOFF = chr(19) FEND = 0xC0 FESC = 0xDB TFEND = 0xDC TFESC = 0xDD TNC_DEBUG = True def kiss_escape_frame(frame): escaped = "" for char in frame: if ord(char) == FEND: escaped += chr(FESC) escaped += chr(TFEND) elif ord(char) == FESC: escaped += chr(FESC) escaped += chr(TFESC) else: escaped += char return escaped def kiss_send_frame(frame, port=0): cmd = (port & 0x0F) << 4 frame = kiss_escape_frame(frame) buf = struct.pack("BB", FEND, cmd) + frame + struct.pack("B", FEND) if TNC_DEBUG: print "[TNC] Sending:" utils.hexprint(buf) return buf def kiss_buf_has_frame(buf): return buf.count(chr(FEND)) >=2 def kiss_recv_frame(buf): if not buf: return "", "" data = "" inframe = False _buf = "" _lst = "0" # Make sure we don't choke trying to ord() this for char in buf: if ord(char) == FEND: if not inframe: inframe = True else: data += _buf[1:] _buf = "" inframe = False elif ord(char) == FESC: pass # Ignore this and wait for the next character elif ord(_lst) == FESC: if ord(char) == TFEND: _buf += chr(FEND) elif ord(char) == TFESC: _buf += chr(FESC) else: print "[TNC] Bad escape of 0x%x" % ord(char) break elif inframe: _buf += char else: print "[TNC] Out-of-frame garbage: 0x%x" % ord(char) _lst = char if TNC_DEBUG: print "[TNC] Data:" utils.hexprint(data) if not inframe and _buf: # There was not a partial frame started at the end of the data print "[TNC] Dumping non-frame data trailer" utils.hexprint(_buf) _buf = "" return data, _buf class TNCSerial(serial.Serial): def __init__(self, **kwargs): if "tncport" in kwargs.keys(): self.__tncport = kwargs["tncport"] del kwargs["tncport"] else: self.__tncport = 0 serial.Serial.__init__(self, **kwargs) self.__buffer = "" self.__tstamp = 0 def reconnect(self): pass def write(self, data): serial.Serial.write(self, kiss_send_frame(data, self.__tncport)) def read(self, size): if self.__buffer: print "Buffer is %i before read" % len(self.__buffer) self.__buffer += serial.Serial.read(self, 1024) framedata = "" if kiss_buf_has_frame(self.__buffer): framedata, self.__buffer = kiss_recv_frame(self.__buffer) elif len(self.__buffer) > 0: print "[TNC] Buffer partially-filled (%i b)" % len(self.__buffer) return framedata class SWFSerial(serial.Serial): __swf_debug = False def __init__(self, **kwargs): print "Software XON/XOFF control initialized" try: serial.Serial.__init__(self, **kwargs) except TypeError: if "writeTimeout" in kwargs: del kwargs["writeTimeout"] serial.Serial.__init__(self, **kwargs) else: print "Unknown TypeError from Serial.__init__: %s" % e raise e self.state = True self.xoff_limit = 15 def reconnect(self): self.close() time.sleep(0.5) self.open() def is_xon(self): time.sleep(0.01) if serial.Serial.inWaiting(self) == 0: return self.state char = serial.Serial.read(self, 1) if char == ASCII_XOFF: if self.__swf_debug: print "************* Got XOFF" self.state = False elif char == ASCII_XON: if self.__swf_debug: print "------------- Got XON" self.state = True elif len(char) == 1: print "Aiee! Read a non-XOFF char: 0x%02x `%s`" % (ord(char), char) self.state = True print "Assuming IXANY behavior" return self.state def _write(self, data): chunk = 8 pos = 0 while pos < len(data): if self.__swf_debug: print "Sending %i-%i of %i" % (pos, pos+chunk, len(data)) serial.Serial.write(self, data[pos:pos+chunk]) self.flush() pos += chunk start = time.time() while not self.is_xon(): if self.__swf_debug: print "We're XOFF, waiting: %s" % self.state time.sleep(0.01) if (time.time() - start) > self.xoff_limit: #print "XOFF for too long, breaking loop!" #raise DataPathIOError("Write error (flow)") print "XOFF for too long, assuming XON" self.state = True def write(self, data): old_to = self.timeout self.timeout = 0.01 self._write(data) self.timeout = old_to def read(self, len): return serial.Serial.read(self, len) class DataPath(object): def __init__(self, pathspec, timeout=0.25): self.timeout = timeout self.pathspec = pathspec self.can_reconnect = True def connect(self): raise DataPathNotConnectedError("Can't connect base class") def disconnect(self): raise DataPathNotConnectedError("Can't disconnect base class") def read(self, count): raise DataPathIOError("Can't read from base class") def write(self, buf): raise DataPathIOError("Can't write to base class") def flush(self, buf): raise DataPathIOError("Can't flush the base class") def is_connected(self): return False def __str__(self): return "--" class AGWDataPath(DataPath): def __init__(self, pathspec, timeout=0): DataPath.__init__(self, pathspec, timeout) agw, self._addr, self._port = pathspec.split(":") self._agw = None def connect(self): try: self._agw = agw.AGWConnection(self._addr, int(self._port), self.timeout) self._agw.enable_raw() except Exception, e: print "AGWPE exception on connect: %s" % e raise DataPathNotConnectedError("Unable to connect to AGWPE") def disconnect(self): if self._agw: self._agw.close() def reconnect(self): self.disconnect() self.connect() def read(self, count): return agw.receive_data(self._agw) def write(self, buf): agw.transmit_data(self._agw, "CQ", ["SRC", "RELAY"], buf) def is_connected(self): return bool(self._agw) def __str__(self): return "[AGWPE %s:%s]" % (self._addr, self._port) def get_agw_connection(self): return self._agw def read_all_waiting(self): return agw.receive_data(self._agw) class SerialDataPath(DataPath): def __init__(self, pathspec, timeout=0.25): DataPath.__init__(self, pathspec, timeout) (self.port, self.baud) = pathspec self._serial = None def connect(self): try: self._serial = SWFSerial(port=self.port, baudrate=self.baud, timeout=self.timeout, writeTimeout=self.timeout, xonxoff=0) except Exception, e: print "Serial exception on connect: %s" % e raise DataPathNotConnectedError("Unable to open serial port") def disconnect(self): if self._serial: self._serial.close() self._serial = None def reconnect(self): return def read(self, count): try: data = self._serial.read(count) except Exception, e: print "Serial read exception: %s" % e utils.log_exception() raise DataPathIOError("Failed to read from serial port") return data def read_all_waiting(self): data = self.read(1) data += self.read(self._serial.inWaiting()) return data def write(self, buf): try: self._serial.write(buf) except Exception ,e: print "Serial write exception: %s" % e utils.log_exception() raise DataPathIOError("Failed to write to serial port") def is_connected(self): return self._serial != None def flush(self): self._serial.flush() def __str__(self): return "[SERIAL %s@%s]" % (self.port, self.baud) class TNCDataPath(SerialDataPath): def connect(self): if ":" in self.port: self.port, tncport = self.port.split(":", 1) tncport = int(tncport) else: tncport = 0 try: self._serial = TNCSerial(port=self.port, tncport=tncport, baudrate=self.baud, timeout=self.timeout, writeTimeout=self.timeout*10, xonxoff=0) except Exception, e: print "TNC exception on connect: %s" % e utils.log_exception() raise DataPathNotConnectedError("Unable to open serial port") def __str__(self): return "[TNC %s@%s]" % (self.port, self.baud) # FIXME: Move this fcstab = [ 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 ] def compute_fcs(data): fcs = 0xffff for byte in data: fcs = (fcs >> 8 ) ^ fcstab[(fcs ^ ord(byte)) & 0xff] return (~fcs) & 0xffff class TNCAX25DataPath(TNCDataPath): def __init__(self, pathspec, **kwargs): (port, rate, self.__call, self.__path) = pathspec self.__buffer = "" TNCDataPath.__init__(self, (port, rate), **kwargs) def __str__(self): return "[TNC-AX25 %s@%s>%s]" % (self.port, self.baud, self.__path) def write(self, buf): spath = [self.__call,] + self.__path.split(",") src = "" for scall in spath: c, s = agw.ssid(scall) src += "".join([chr(ord(x) << 1) for x in c]) src += agw.encode_ssid(s, spath[-1] == scall) c, s = agw.ssid("DRATS") dst = "".join([chr(ord(x) << 1) for x in c]) dst += agw.encode_ssid(s) hdr = struct.pack("7s%isBB" % len(src), dst, # Dest call src, # Source path 0x03, # Control 0xF0) # PID: No layer 3 fcs = compute_fcs(hdr + buf) data = hdr + buf + struct.pack(">H", fcs) #print "Transmitting AX.25 Frame:" #utils.hexprint(data) TNCDataPath.write(self, data) def read(self, count): while len(self.__buffer) < count: chunk = TNCDataPath.read(self, 1) if not chunk: break self.__buffer += chunk data = self.__buffer[:count] self.__buffer = self.__buffer[count:] return data class SocketDataPath(DataPath): def __init__(self, pathspec, timeout=0.25): DataPath.__init__(self, pathspec, timeout) self._socket = None if isinstance(pathspec, socket.socket): self._socket = pathspec self._socket.settimeout(self.timeout) self.can_reconnect = False self.host = "(incoming)" self.port = 0 elif len(pathspec) == 2: (self.host, self.port) = pathspec self.call = self.passwd = "UNKNOWN" else: (self.host, self.port, self.call, self.passwd) = pathspec def reconnect(self): if not self.can_reconnect: return self.disconnect() time.sleep(0.5) self.connect() def do_auth(self): def readline(_s, to=30): t = time.time() line = "" while ("\n" not in line) and ((time.time() - t) < to): try: _d = _s.recv(32) if not _d: break except socket.timeout: continue line += _d return line.strip() def getline(_s, to=30): line = readline(_s, to) try: code, string = line.split(" ", 1) code = int(code) except Exception, e: print "Error parsing line %s: %s" % (line, e) raise DataPathNotConnectedError("Conversation error") return code, string try: c, l = getline(self._socket) except DataPathNotConnectedError: print "Assuming an old-school ratflector for now" return if c == 100: print "Host does not require authentication" return elif c != 101: raise DataPathNotConnectedError("Unknown response code %i" % c) print "Doing authentication" print "Sending username: %s" % self.call self._socket.send("USER %s\r\n" % self.call) c, l = getline(self._socket) if c == 200: print "Host did not require a password" elif c != 102: raise DataPathNotConnectedError("User rejected username") print "Sending password: %s" % ("*" * len(self.passwd)) self._socket.send("PASS %s\r\n" % self.passwd) c, l = getline(self._socket) print "Host responded: %i %s" % (c, l) if c != 200: raise DataPathNotConnectedError("Authentication failed: %s" % l) def connect(self): try: self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket.connect((self.host, self.port)) self._socket.settimeout(self.timeout) except Exception, e: print "Socket connect failed: %s" % e self._socket = None raise DataPathNotConnectedError("Unable to connect (%s)" % e) if self.passwd is not None: self.do_auth() def disconnect(self): if self._socket: self._socket.close() self._socket = None def read(self, count): data = "" end = time.time() + self.timeout if not self._socket: raise DataPathIOError("Socket closed") self._socket.setblocking(True) self._socket.settimeout(self.timeout) while len(data) < count: try: x = time.time() inp = self._socket.recv(count - len(data)) except socket.timeout: if time.time() > end: break else: continue except Exception, e: raise DataPathIOError("Socket error: %s" % e) if inp == "": raise DataPathIOError("Socket disconnected") end = time.time() + self.timeout data += inp return data def read_all_waiting(self): if not self._socket: raise DataPathIOError("Socket disconnected") self._socket.setblocking(False) r, w, x = select.select([self._socket], [], [], self.timeout) if not r: return "" data = "" while True: try: d = self._socket.recv(4096) except Exception, e: break if not d: raise DataPathIOError("Socket disconnected") data += d return data def write(self, buf): try: self._socket.sendall(buf) except Exception, e: print "Socket write failed: %s" % e raise DataPathIOError("Socket write failed") return def is_connected(self): return self._socket != None def flush(self): pass def __str__(self): try: addr, port = self._socket.getpeername() return "[NET %s:%i]" % (addr, port) except: return "[NET closed]" d-rats-0.3.3/d_rats/config.py000066400000000000000000001620371160617671700160270ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gtk.glade import gobject import ConfigParser import os import random import utils import miscwidgets import inputdialog import platform import geocode_ui import config_tips import spell from d_rats.ui.main_common import display_error BAUD_RATES = ["1200", "2400", "4800", "9600", "19200", "38400", "115200"] _DEF_USER = { "name" : "A. Mateur", "callsign" : "", "latitude" : "41.6970", "longitude" : "-72.7312", "altitude" : "0", "units" : _("Imperial"), } _DEF_PREFS = { "download_dir" : ".", "blinkmsg" : "False", "noticere" : "", "ignorere" : "", "signon" : _("Online (D-RATS)"), "signoff" : _("Going offline (D-RATS)"), "dosignon" : "True", "dosignoff" : "True", "incomingcolor" : "#00004444FFFF", "outgoingcolor": "#DDDD44441111", "noticecolor" : "#0000660011DD", "ignorecolor" : "#BB88BB88BB88", "callsigncolor" : "#FFDD99CC77CC", "brokencolor" : "#FFFFFFFF3333", "logenabled" : "True", "debuglog" : "False", "eolstrip" : "True", "font" : "Sans 12", "callsigns" : "%s" % str([(True , "US")]), "logresume" : "True", "scrollback" : "1024", "restore_stations" : "True", "useutc" : "False", "language" : "English", "allow_remote_files" : "True", "blink_chat" : "True", "blink_messages" : "True", "blink_files" : "True", "blink_event" : "False", "chat_showstatus" : "True", "chat_timestamp" : "True", "msg_include_reply" : "False", "msg_allow_wl2k" : "True", "msg_allow_pop3" : "True", "msg_wl2k_server" : "server.winlink.org", "msg_wl2k_ssid" : "", "msg_wl2k_port" : "8772", "toolbar_button_size" : "Default", "check_spelling" : "False", "confirm_exit" : "False", "msg_wl2k_rmscall" : "", "msg_wl2k_rmsport" : "", } _DEF_SETTINGS = { "socket_pw" : "", "ddt_block_size" : "512", "ddt_block_outlimit" : "4", "encoding" : "yenc", "compression" : "True", "gpsport" : "", "gpsenabled" : "False", "gpsportspeed" : "4800", "aprssymtab" : "/", "aprssymbol" : ">", "compatmode" : "False", "inports" : "[]", "outports" : "[]", "sockflush" : "0.5", "pipelinexfers" : "True", "mapdir" : os.path.join(platform.get_platform().config_dir(), "maps"), "warmup_length" : "8", "warmup_timeout" : "3", "force_delay" : "-2", "ping_info" : "", "smtp_server" : "", "smtp_replyto" : "", "smtp_tls" : "False", "smtp_username" : "", "smtp_password" : "", "smtp_port" : "25", "smtp_dogw" : "False", "sniff_packets" : "False", "map_tile_ttl" : "720", "msg_flush" : "60", "msg_forward" : "False", "form_logo_dir" : os.path.join(platform.get_platform().config_dir(), "logos"), "default_gps_comment" : "D-RATS Station", "http_proxy" : "", "station_msg_ttl" : "3600", "timestamp_positions" : "False", "msg_wl2k_mode" : "Network", "qst_size_limit" : "2048", "msg_pop3_server" : "False", "msg_pop3_port" : "9110", "msg_smtp_server" : "False", "msg_smtp_port" : "9025", "delete_from" : "", "remote_admin_passwd" : "", } _DEF_STATE = { "main_size_x" : "640", "main_size_y" : "400", "main_advanced" : "200", "filters" : "[]", "show_all_filter" : "False", "connected_inet" : "True", "qsts_enabled" : "True", "sidepane_visible" : "True", "status_msg" : "Online (D-RATS)", "status_state" : "Online", "events_sort" : str(int(gtk.SORT_DESCENDING)), "form_email_x" : "600", "form_email_y" : "500", } _DEF_SOUNDS = { "messages" : "", "messages_enabled" : "False", "chat" : "", "chat_enabled" : "False", "files" : "", "files_enabled" : "False", } DEFAULTS = { "user" : _DEF_USER, "prefs" : _DEF_PREFS, "settings" : _DEF_SETTINGS, "state" : _DEF_STATE, "quick" : {}, "tcp_in" : {}, "tcp_out" : {}, "incoming_email" : {}, "sounds" : _DEF_SOUNDS, "ports" : { "ports_0" : "True,net:ref.d-rats.com:9000,,False,False,RAT" }, } if __name__ == "__main__": import gettext gettext.install("D-RATS") def color_string(color): try: return color.to_string() except: return "#%04x%04x%04x" % (color.red, color.green, color.blue) def load_portspec(wtree, portspec, info, name): namewidget = wtree.get_widget("name") namewidget.set_text(name) namewidget.set_sensitive(False) tsel = wtree.get_widget("type") if portspec.startswith("net:"): tsel.set_active(1) net, host, port = portspec.split(":") wtree.get_widget("net_host").set_text(host) wtree.get_widget("net_port").set_value(int(port)) wtree.get_widget("net_pass").set_text(info) elif portspec.startswith("tnc"): tsel.set_active(2) if len(portspec.split(":")) == 3: tnc, port, tncport = portspec.split(":", 2) path = "" else: tnc, port, tncport, path = portspec.split(":", 3) wtree.get_widget("tnc_port").child.set_text(port) wtree.get_widget("tnc_tncport").set_value(int(tncport)) utils.combo_select(wtree.get_widget("tnc_rate"), info) wtree.get_widget("tnc_ax25path").set_text(path.replace(";", ",")) if portspec.startswith("tnc-ax25"): wtree.get_widget("tnc_ax25").set_active(True) elif portspec.startswith("agwpe:"): tsel.set_active(4) agw, addr, port = portspec.split(":") wtree.get_widget("agw_addr").set_text(addr) wtree.get_widget("agw_port").set_value(int(port)) else: tsel.set_active(0) wtree.get_widget("serial_port").child.set_text(portspec) utils.combo_select(wtree.get_widget("serial_rate"), info) def prompt_for_port(portspec=None, info=None, pname=None): p = os.path.join(platform.get_platform().source_dir(), "ui/addport.glade") wtree = gtk.glade.XML(p, "addport", "D-RATS") ports = platform.get_platform().list_serial_ports() sportsel = wtree.get_widget("serial_port") tportsel = wtree.get_widget("tnc_port") sportlst = sportsel.get_model() tportlst = tportsel.get_model() sportlst.clear() tportlst.clear() for port in ports: sportlst.append((port,)) tportlst.append((port,)) if ports: sportsel.set_active(0) tportsel.set_active(0) sratesel = wtree.get_widget("serial_rate") tratesel = wtree.get_widget("tnc_rate") tprotsel = wtree.get_widget("tnc_ax25") tnc_ax25 = wtree.get_widget("tnc_ax25") tnc_path = wtree.get_widget("tnc_ax25path") tnc_ax25.connect("toggled", lambda b: tnc_path.set_sensitive(b.get_active())) sratesel.set_active(3) tratesel.set_active(3) netaddr = wtree.get_widget("net_host") netport = wtree.get_widget("net_port") netpass = wtree.get_widget("net_pass") agwaddr = wtree.get_widget("agw_addr") agwport = wtree.get_widget("agw_port") agwport.set_value(8000) descriptions = [ "A D-STAR radio connected to a serial port", "A network link to a ratflector instance", "A KISS-mode TNC connected to a serial port", "A locally-attached dongle", "A TNC attached to an AGWPE server", ] tablist = [_("Serial"), _("Network"), _("TNC"), _("Dongle"), _("AGWPE")] def chg_type(tsel, tabs, desc): print "Changed to %s" % tsel.get_active_text() tabs.set_current_page(tsel.get_active()) desc.set_markup("%s" % \ descriptions[tsel.get_active()]) name = wtree.get_widget("name") desc = wtree.get_widget("typedesc") ttncport = wtree.get_widget("tnc_tncport") tabs = wtree.get_widget("editors") tabs.set_show_tabs(False) tsel = wtree.get_widget("type") tsel.set_active(0) tsel.connect("changed", chg_type, tabs, desc) if portspec: load_portspec(wtree, portspec, info, pname) elif pname is False: name.set_sensitive(False) d = wtree.get_widget("addport") chg_type(tsel, tabs, desc) r = d.run() t = tablist[tsel.get_active()] if t == _("Serial"): portspec = sportsel.get_active_text(), sratesel.get_active_text() elif t == _("Network"): portspec = "net:%s:%i" % (netaddr.get_text(), netport.get_value()), \ netpass.get_text() elif t == _("TNC"): if tprotsel.get_active(): digi_path = tnc_path.get_text().replace(",", ";") portspec = "tnc-ax25:%s:%i:%s" % (tportsel.get_active_text(), ttncport.get_value(), digi_path), \ tratesel.get_active_text() else: portspec = "%s:%s:%i" % (type, tportsel.get_active_text(), ttncport.get_value()), \ tratesel.get_active_text() elif t == _("Dongle"): portspec = "dongle:", "" elif t == _("AGWPE"): portspec = "agwpe:%s:%i" % (agwaddr.get_text(), agwport.get_value()), "" portspec = (name.get_text(),) + portspec d.destroy() if r: return portspec else: return None, None, None def disable_with_toggle(toggle, widget): toggle.connect("toggled", lambda t, w: w.set_sensitive(t.get_active()), widget) widget.set_sensitive(toggle.get_active()) def disable_by_combo(combo, map): # Expects a map like: # map = { # "value1" : [el1, el2], # "value2" : [el3, el4], # } def set_disables(combo, map): for i in map.values(): for j in i: j.set_sensitive(False) for i in map[combo.get_active_text()]: i.set_sensitive(True) combo.connect("changed", set_disables, map) set_disables(combo, map) class AddressLookup(gtk.Button): def __init__(self, caption, latw, lonw, window=None): gtk.Button.__init__(self, caption) self.connect("clicked", self.clicked, latw, lonw, window) def clicked(self, me, latw, lonw, window): aa = geocode_ui.AddressAssistant() aa.set_transient_for(window) r = aa.run() if r == gtk.RESPONSE_OK: latw.latlon.set_text("%.5f" % aa.lat) lonw.latlon.set_text("%.5f" % aa.lon) class DratsConfigWidget(gtk.HBox): def __init__(self, config, sec, name, have_revert=False): gtk.HBox.__init__(self, False, 2) self.do_not_expand = False self.config = config self.vsec = sec self.vname = name self._widget = None self.config.widgets.append(self) if not config.has_section(sec): config.add_section(sec) if name is not None: if not config.has_option(sec, name): self._revert() else: self.value = config.get(sec, name) else: self.value = None if have_revert: rb = gtk.Button(None, gtk.STOCK_REVERT_TO_SAVED) rb.connect("clicked", self._revert) rb.show() self.pack_end(rb, 0, 0, 0) def _revert(self, button=None): try: self.value = DEFAULTS[self.vsec][self.vname] except KeyError: print "DEFAULTS has no %s/%s" % (self.vsec, self.vname) self.value = "" if not self._widget: print "AAACK: No _widget in revert" return if isinstance(self._widget, gtk.Entry): self._widget.set_text(str(self.value)) elif isinstance(self._widget, gtk.SpinButton): self._widget.set_value(float(self.value)) elif isinstance(self._widget, gtk.CheckButton): self._widget.set_active(self.value.upper() == "TRUE") elif isinstance(self._widget, miscwidgets.FilenameBox): self._widget.set_filename(self.value) else: print "AAACK: I don't know how to do a %s" % self._widget.__class__ def save(self): #print "Saving %s/%s: %s" % (self.vsec, self.vname, self.value) self.config.set(self.vsec, self.vname, self.value) def set_value(self, value): pass def add_text(self, limit=0, hint=None): def changed(entry): if entry.get_text() == hint: self.value = "" else: self.value = entry.get_text() w = gtk.Entry(limit) w.connect("changed", changed) w.set_text(self.value) w.set_size_request(50, -1) w.show() self._widget = w if hint: utils.set_entry_hint(w, hint, bool(self.value)) self.pack_start(w, 1, 1, 1) def add_upper_text(self, limit=0): def changed(entry): self.value = entry.get_text().upper() w = gtk.Entry(limit) w.connect("changed", changed) w.set_text(self.value) w.set_size_request(50, -1) w.show() self._widget = w self.pack_start(w, 1, 1, 1) def add_pass(self, limit=0): def changed(entry): self.value = entry.get_text() w = gtk.Entry(limit) w.connect("changed", changed) w.set_text(self.value) w.set_visibility(False) w.set_size_request(50, -1) w.show() self._widget = w self.pack_start(w, 1, 1, 1) def add_combo(self, choices=[], editable=False, size=80): def changed(box): self.value = box.get_active_text() if self.value not in choices: choices.append(self.value) w = miscwidgets.make_choice(choices, editable, self.value) w.connect("changed", changed) w.set_size_request(size, -1) w.show() self._widget = w self.pack_start(w, 1, 1, 1) def add_bool(self, label=None): if label is None: label = _("Enabled") def toggled(but, confwidget): confwidget.value = str(but.get_active()) w = gtk.CheckButton(label) w.connect("toggled", toggled, self) w.set_active(self.value == "True") w.show() self._widget = w self.do_not_expand = True self.pack_start(w, 1, 1, 1) def add_coords(self): def changed(entry, confwidget): try: confwidget.value = "%3.6f" % entry.value() except Exception, e: print "Invalid Coords: %s" % e confwidget.value = "0" w = miscwidgets.LatLonEntry() w.connect("changed", changed, self) print "Setting LatLon value: %s" % self.value w.set_text(self.value) print "LatLon text: %s" % w.get_text() w.show() # Dirty ugly hack! self.latlon = w self.pack_start(w, 1, 1, 1) def add_numeric(self, min, max, increment, digits=0): def value_changed(sb): self.value = "%f" % sb.get_value() adj = gtk.Adjustment(float(self.value), min, max, increment, increment) w = gtk.SpinButton(adj, digits) w.connect("value-changed", value_changed) w.show() self._widget = w self.pack_start(w, 1, 1, 1) def add_color(self): def color_set(but): self.value = color_string(but.get_color()) w = gtk.ColorButton() w.set_color(gtk.gdk.color_parse(self.value)) w.connect("color-set", color_set) w.show() self.pack_start(w, 1, 1, 1) def add_font(self): def font_set(but): self.value = but.get_font_name() w = gtk.FontButton() w.set_font_name(self.value) w.connect("font-set", font_set) w.show() self.pack_start(w, 1, 1, 1) def add_path(self): def filename_changed(box): self.value = box.get_filename() w = miscwidgets.FilenameBox(find_dir=True) w.set_filename(self.value) w.connect("filename-changed", filename_changed) w.show() self._widget = w self.pack_start(w, 1, 1, 1) def add_sound(self): def filename_changed(box): self.value = box.get_filename() def test_sound(button): print "Testing playback of %s" % self.value p = platform.get_platform() p.play_sound(self.value) w = miscwidgets.FilenameBox(find_dir=False) w.set_filename(self.value) w.connect("filename-changed", filename_changed) w.show() b = gtk.Button(_("Test"), gtk.STOCK_MEDIA_PLAY) b.connect("clicked", test_sound) b.show() box = gtk.HBox(False, 2) box.show() box.pack_start(w, 1, 1, 1) box.pack_start(b, 0, 0, 0) self.pack_start(box, 1, 1, 1) class DratsListConfigWidget(DratsConfigWidget): def __init__(self, config, section): try: DratsConfigWidget.__init__(self, config, section, None) except ConfigParser.NoOptionError: pass def convert_types(self, coltypes, values): newvals = [] i = 0 while i < len(values): gtype, label = coltypes[i] value = values[i] try: if gtype == gobject.TYPE_INT: value = int(value) elif gtype == gobject.TYPE_FLOAT: value = float(value) elif gtype == gobject.TYPE_BOOLEAN: value = eval(value) except ValueError, e: print "Failed to convert %s for %s: %s" % (value, label, e) return [] i += 1 newvals.append(value) return newvals def set_sort_column(self, col): self.listw.set_sort_column(col) def add_list(self, cols, make_key=None): def item_set(lw, key): pass w = miscwidgets.KeyedListWidget(cols) def foo(*args): return w.connect("item-toggled", foo) options = self.config.options(self.vsec) for option in options: vals = self.config.get(self.vsec, option).split(",", len(cols)) vals = self.convert_types(cols[1:], vals) if not vals: continue try: if make_key: key = make_key(vals) else: key = vals[0] w.set_item(key, *tuple(vals)) except Exception, e: print "Failed to set item '%s': %s" % (str(vals), e) w.connect("item-set", item_set) w.show() self.pack_start(w, 1, 1, 1) self.listw = w return w def save(self): for opt in self.config.options(self.vsec): self.config.remove_option(self.vsec, opt) count = 0 for key in self.listw.get_keys(): vals = self.listw.get_item(key) vals = [str(x) for x in vals] value = ",".join(vals[1:]) label = "%s_%i" % (self.vsec, count) print "Setting %s: %s" % (label, value) self.config.set(self.vsec, label, value) count += 1 class DratsPanel(gtk.Table): INITIAL_ROWS = 13 INITIAL_COLS = 2 def __init__(self, config): gtk.Table.__init__(self, self.INITIAL_ROWS, self.INITIAL_COLS) self.config = config self.vals = [] self.row = 0 self.rows = self.INITIAL_ROWS def mv(self, title, *args): if self.row+1 == self.rows: self.rows += 1 print "Resizing box to %i" % self.rows self.resize(self.rows, 2) hbox = gtk.HBox(False, 2) lab = gtk.Label(title) lab.show() self.attach(lab, 0, 1, self.row, self.row+1, gtk.SHRINK, gtk.SHRINK, 5) for i in args: i.show() if isinstance(i, DratsConfigWidget): if i.do_not_expand: hbox.pack_start(i, 0, 0, 0) else: hbox.pack_start(i, 1, 1, 0) self.vals.append(i) else: hbox.pack_start(i, 0, 0, 0) hbox.show() self.attach(hbox, 1, 2, self.row, self.row+1, yoptions=gtk.SHRINK) self.row += 1 def mg(self, title, *args): if len(args) % 2: raise Exception("Need label,widget pairs") table = gtk.Table(len(args)/2, 2) row = 0 k = { "yoptions" : gtk.SHRINK, "xoptions" : gtk.SHRINK, "xpadding" : 10, "ypadding" : 0} for i in range(0, len(args), 2): label = gtk.Label(args[i]) widget = args[i+1] label.show() widget.show() table.attach(label, 0, 1, row, row+1, **k) table.attach(widget, 1, 2, row, row+1, **k) row += 1 table.show() frame = gtk.Frame(title) frame.show() frame.add(table) self.attach(frame, 1, 2, self.row, self.row+1) class DratsPrefsPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) val = DratsConfigWidget(config, "user", "callsign") val.add_upper_text(8) self.mv(_("Callsign"), val) val = DratsConfigWidget(config, "user", "name") val.add_text() self.mv(_("Name"), val) val1 = DratsConfigWidget(config, "prefs", "dosignon") val1.add_bool() val2 = DratsConfigWidget(config, "prefs", "signon") val2.add_text() self.mv(_("Sign-on Message"), val1, val2) disable_with_toggle(val1._widget, val2._widget) val1 = DratsConfigWidget(config, "prefs", "dosignoff") val1.add_bool() val2 = DratsConfigWidget(config, "prefs", "signoff") val2.add_text() self.mv(_("Sign-off Message"), val1, val2) disable_with_toggle(val1._widget, val2._widget) val = DratsConfigWidget(config, "user", "units") val.add_combo([_("Imperial"), _("Metric")]) self.mv(_("Units"), val) val = DratsConfigWidget(config, "prefs", "useutc") val.add_bool() self.mv(_("Show time in UTC"), val) val = DratsConfigWidget(config, "settings", "ping_info") val.add_text(hint=_("Version and OS Info")) self.mv(_("Ping reply"), val) val = DratsConfigWidget(config, "prefs", "language") val.add_combo(["English", "German", "Italiano", "Dutch"]) self.mv(_("Language"), val) mval = DratsConfigWidget(config, "prefs", "blink_messages") mval.add_bool() cval = DratsConfigWidget(config, "prefs", "blink_chat") cval.add_bool() fval = DratsConfigWidget(config, "prefs", "blink_files") fval.add_bool() eval = DratsConfigWidget(config, "prefs", "blink_event") eval.add_bool() self.mg(_("Blink tray on"), _("Incoming Messages"), mval, _("New Chat Messages"), cval, _("Incoming Files"), fval, _("Received Events"), eval) class DratsPathsPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) val = DratsConfigWidget(config, "prefs", "download_dir", True) val.add_path() self.mv(_("File Transfer Path"), val) val = DratsConfigWidget(config, "settings", "mapdir", True) val.add_path() self.mv(_("Map Storage Path"), val) val = DratsConfigWidget(config, "settings", "form_logo_dir", True) val.add_path() self.mv(_("Form Logo Path"), val) class DratsGPSPanel(DratsPanel): def __init__(self, config, window): DratsPanel.__init__(self, config) lat = DratsConfigWidget(config, "user", "latitude") lat.add_coords() self.mv(_("Latitude"), lat) lon = DratsConfigWidget(config, "user", "longitude") lon.add_coords() self.mv(_("Longitude"), lon) geo = AddressLookup(_("Lookup"), lat, lon, window) self.mv(_("Lookup by address"), geo) alt = DratsConfigWidget(config, "user", "altitude") alt.add_numeric(0, 29028, 1) self.mv(_("Altitude"), alt) ports = platform.get_platform().list_serial_ports() val = DratsConfigWidget(config, "settings", "gpsenabled") val.add_bool() self.mv(_("Use External GPS"), val) port = DratsConfigWidget(config, "settings", "gpsport") port.add_combo(ports, True, 120) rate = DratsConfigWidget(config, "settings", "gpsportspeed") rate.add_combo(BAUD_RATES, False) self.mv(_("External GPS"), port, rate) disable_with_toggle(val._widget, port._widget) disable_with_toggle(val._widget, rate._widget) val1 = DratsConfigWidget(config, "settings", "aprssymtab") val1.add_text(1) val2 = DratsConfigWidget(config, "settings", "aprssymbol") val2.add_text(1) self.mv(_("GPS-A Symbol"), gtk.Label(_("Table:")), val1, gtk.Label(_("Symbol:")), val2) val = DratsConfigWidget(config, "settings", "map_tile_ttl") val.add_numeric(0, 9999999999999, 1) self.mv(_("Freshen map after"), val, gtk.Label(_("hours"))) def gps_comment_from_dprs(button, val): import qst dprs = qst.do_dprs_calculator(config.get("settings", "default_gps_comment")) if dprs is not None: config.set("settings", "default_gps_comment", dprs) val._widget.set_text(dprs) val = DratsConfigWidget(config, "settings", "default_gps_comment") val.add_text(20) but = gtk.Button(_("DPRS")) but.connect("clicked", gps_comment_from_dprs, val) self.mv(_("Default GPS comment"), val, but) val = DratsConfigWidget(config, "settings", "timestamp_positions") val.add_bool() self.mv(_("Timestamp Positions"), val) class DratsAppearancePanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) val = DratsConfigWidget(config, "prefs", "noticere") val.add_text() self.mv(_("Notice RegEx"), val) val = DratsConfigWidget(config, "prefs", "ignorere") val.add_text() self.mv(_("Ignore RegEx"), val) colors = ["Incoming", "Outgoing", "Notice", "Ignore", "Callsign", "Broken"] # Mark these strings so they get picked up and become available # to the _(i) below _trans_colors = [_("Incoming Color"), _("Outgoing Color"), _("Notice Color"), _("Ignore Color"), _("Callsign Color"), _("Broken Color")] for i in colors: low = i.lower() val = DratsConfigWidget(config, "prefs", "%scolor" % low) val.add_color() self.mv(_("%s Color" % i), val) sizes = [_("Default"), _("Large"), _("Small")] val = DratsConfigWidget(config, "prefs", "toolbar_button_size") val.add_combo(sizes, False) self.mv(_("Toolbar buttons"), val) val = DratsConfigWidget(config, "prefs", "check_spelling") val.add_bool() self.mv(_("Check spelling"), val) sp = spell.get_spell() val._widget.set_sensitive(sp.test()) val = DratsConfigWidget(config, "prefs", "confirm_exit") val.add_bool() self.mv(_("Confirm exit"), val) class DratsChatPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) val = DratsConfigWidget(config, "prefs", "logenabled") val.add_bool() self.mv(_("Log chat traffic"), val) val = DratsConfigWidget(config, "prefs", "logresume") val.add_bool() self.mv(_("Load log tail"), val) val = DratsConfigWidget(config, "prefs", "font") val.add_font() self.mv(_("Chat font"), val) val = DratsConfigWidget(config, "prefs", "scrollback") val.add_numeric(0, 9999, 1) self.mv(_("Scrollback Lines"), val) val = DratsConfigWidget(config, "prefs", "chat_showstatus") val.add_bool() self.mv(_("Show status updates in chat"), val) val = DratsConfigWidget(config, "prefs", "chat_timestamp") val.add_bool() self.mv(_("Timestamp chat messages"), val) val = DratsConfigWidget(config, "settings", "qst_size_limit") val.add_numeric(1, 9999, 1) self.mv(_("QST Size Limit"), val) class DratsSoundPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) def do_snd(k, l): snd = DratsConfigWidget(config, "sounds", k) snd.add_sound() enb = DratsConfigWidget(config, "sounds", "%s_enabled" % k) enb.add_bool() self.mv(l, snd, enb) do_snd("chat", _("Chat activity")) do_snd("messages", _("Message activity")) do_snd("files", _("File activity")) class DratsRadioPanel(DratsPanel): INITIAL_ROWS = 3 def mv(self, title, *widgets): self.attach(widgets[0], 0, 2, 0, 1) widgets[0].show() if len(widgets) > 1: box = gtk.HBox(True, 2) for i in widgets[1:]: box.pack_start(i, 0, 0, 0) i.show() box.show() self.attach(box, 0, 2, 1, 2, yoptions=gtk.SHRINK) def but_add(self, button, lw): name, port, info = prompt_for_port() if name: lw.set_item(name, True, port, info, False, False, name) def but_mod(self, button, lw): values = lw.get_item(lw.get_selected()) print "Values: %s" % str(values) name, port, info = prompt_for_port(values[2], values[3], values[6]) if name: lw.set_item(values[6], values[1], port, info, values[4], values[5], values[6]) def but_rem(self, button, lw): lw.del_item(lw.get_selected()) def __init__(self, config): DratsPanel.__init__(self, config) cols = [(gobject.TYPE_STRING, "ID"), (gobject.TYPE_BOOLEAN, _("Enabled")), (gobject.TYPE_STRING, _("Port")), (gobject.TYPE_STRING, _("Settings")), (gobject.TYPE_BOOLEAN, _("Sniff")), (gobject.TYPE_BOOLEAN, _("Raw Text")), (gobject.TYPE_STRING, _("Name"))] lab = gtk.Label(_("Configure data paths below. This may include any number of serial-attached radios and network-attached proxies.")) val = DratsListConfigWidget(config, "ports") def make_key(vals): return vals[5] lw = val.add_list(cols, make_key) add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", self.but_add, lw) mod = gtk.Button(_("Edit"), gtk.STOCK_EDIT) mod.connect("clicked", self.but_mod, lw) rem = gtk.Button(_("Remove"), gtk.STOCK_DELETE) rem.connect("clicked", self.but_rem, lw) val.set_sort_column(6); self.mv(_("Paths"), val, add, mod, rem) lw.set_resizable(1, False) class DratsTransfersPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) val = DratsConfigWidget(config, "settings", "ddt_block_size", True) val.add_numeric(32, 4096, 32) self.mv(_("Block size"), val) val = DratsConfigWidget(config, "settings", "ddt_block_outlimit", True) val.add_numeric(1, 32, 1) self.mv(_("Pipeline blocks"), val) val = DratsConfigWidget(config, "prefs", "allow_remote_files") val.add_bool() self.mv(_("Remote file transfers"), val) val = DratsConfigWidget(config, "settings", "warmup_length", True) val.add_numeric(0, 64, 8) self.mv(_("Warmup Length"), val) val = DratsConfigWidget(config, "settings", "warmup_timeout", True) val.add_numeric(0, 16, 1) self.mv(_("Warmup timeout"), val) val = DratsConfigWidget(config, "settings", "force_delay", True) val.add_numeric(-32, 32, 1) self.mv(_("Force transmission delay"), val) val = DratsConfigWidget(config, "settings", "delete_from") val.add_text() self.mv(_("Allow file deletes from"), val) val = DratsConfigWidget(config, "settings", "remote_admin_passwd") val.add_pass() self.mv(_("Remote admin password"), val) class DratsMessagePanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) vala = DratsConfigWidget(config, "settings", "msg_forward") vala.add_bool() self.mv(_("Automatically forward messages"), vala) val = DratsConfigWidget(config, "settings", "msg_flush") val.add_numeric(15, 9999, 1) lab = gtk.Label(_("seconds")) self.mv(_("Queue flush interval"), val, lab) disable_with_toggle(vala._widget, val._widget) val = DratsConfigWidget(config, "settings", "station_msg_ttl") val.add_numeric(0, 99999, 1) lab = gtk.Label(_("seconds")) self.mv(_("Station TTL"), val, lab) disable_with_toggle(vala._widget, val._widget) val = DratsConfigWidget(config, "prefs", "msg_include_reply") val.add_bool() self.mv(_("Include original in reply"), val) val = DratsConfigWidget(config, "prefs", "msg_allow_pop3") val.add_bool() self.mv(_("Allow POP3 Gateway"), val) vala = DratsConfigWidget(config, "prefs", "msg_allow_wl2k") vala.add_bool() self.mv(_("Allow WL2K Gateway"), vala) wlm = DratsConfigWidget(config, "settings", "msg_wl2k_mode") wlm.add_combo(["Network", "RMS"], False) self.mv(_("WL2K Connection"), wlm) wl2k_servers = [x + ".winlink.org" for x in ["server", "perth", "halifax", "sandiego", "wien"]] srv = DratsConfigWidget(config, "prefs", "msg_wl2k_server") srv.add_combo(wl2k_servers, True) prt = DratsConfigWidget(config, "prefs", "msg_wl2k_port") prt.add_numeric(1, 65535, 1) lab = gtk.Label(_("Port")) self.mv(_("WL2K Network Server"), srv, lab, prt) rms = DratsConfigWidget(config, "prefs", "msg_wl2k_rmscall") rms.add_upper_text(10) lab = gtk.Label(_(" on port ")) ports = [] for port in self.config.options("ports"): spec = self.config.get("ports", port).split(",") if "agwpe" in spec[1]: ports.append(spec[-1]) rpt = DratsConfigWidget(config, "prefs", "msg_wl2k_rmsport") rpt.add_combo(ports, False) self.mv(_("WL2K RMS Station"), rms, lab, rpt) map = { "Network" : [srv._widget, prt._widget], "RMS" : [rms._widget, rpt._widget], } disable_by_combo(wlm._widget, map) disable_with_toggle(vala._widget, wlm._widget) ssids = [""] + [str(x) for x in range(1,11)] val = DratsConfigWidget(config, "prefs", "msg_wl2k_ssid") val.add_combo(ssids, True) self.mv(_("My Winlink SSID"), val) p3s = DratsConfigWidget(config, "settings", "msg_pop3_server") p3s.add_bool() lab = gtk.Label(_("on port")) p3p = DratsConfigWidget(config, "settings", "msg_pop3_port") p3p.add_numeric(1, 65535, 1) self.mv(_("POP3 Server"), p3s, lab, p3p) disable_with_toggle(p3s._widget, p3p._widget) sms = DratsConfigWidget(config, "settings", "msg_smtp_server") sms.add_bool() lab = gtk.Label(_("on port")) smp = DratsConfigWidget(config, "settings", "msg_smtp_port") smp.add_numeric(1, 65535, 1) self.mv(_("SMTP Server"), sms, lab, smp) disable_with_toggle(sms._widget, smp._widget) class DratsNetworkPanel(DratsPanel): pass class DratsTCPPanel(DratsPanel): INITIAL_ROWS = 2 def mv(self, title, *widgets): self.attach(widgets[0], 0, 2, 0, 1) widgets[0].show() if len(widgets) > 1: box = gtk.HBox(True, 2) for i in widgets[1:]: box.pack_start(i, 0, 0, 0) i.show() box.show() self.attach(box, 0, 2, 1, 2, yoptions=gtk.SHRINK) def but_rem(self, button, lw): lw.del_item(lw.get_selected()) def prompt_for(self, fields): d = inputdialog.FieldDialog() for n, t in fields: d.add_field(n, gtk.Entry()) ret = {} done = False while not done and d.run() == gtk.RESPONSE_OK: done = True for n, t in fields: try: s = d.get_field(n).get_text() if not s: raise ValueError("empty") ret[n] = t(s) except ValueError, e: ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK) ed.set_property("text", _("Invalid value for") + " %s: %s" % (n, e)) ed.run() ed.destroy() done = False break d.destroy() if done: return ret else: return None class DratsTCPOutgoingPanel(DratsTCPPanel): def but_add(self, button, lw): values = self.prompt_for([(_("Local Port"), int), (_("Remote Port"), int), (_("Station"), str)]) if values is None: return lw.set_item(str(values[_("Local Port")]), values[_("Local Port")], values[_("Remote Port")], values[_("Station")].upper()) def __init__(self, config): DratsTCPPanel.__init__(self, config) outcols = [(gobject.TYPE_STRING, "ID"), (gobject.TYPE_INT, _("Local")), (gobject.TYPE_INT, _("Remote")), (gobject.TYPE_STRING, _("Station"))] val = DratsListConfigWidget(config, "tcp_out") lw = val.add_list(outcols) add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", self.but_add, lw) rem = gtk.Button(_("Remove"), gtk.STOCK_DELETE) rem.connect("clicked", self.but_rem, lw) self.mv(_("Outgoing"), val, add, rem) class DratsTCPIncomingPanel(DratsTCPPanel): def but_add(self, button, lw): values = self.prompt_for([(_("Port"), int), (_("Host"), str)]) if values is None: return lw.set_item(str(values[_("Port")]), values[_("Port")], values[_("Host")].upper()) def __init__(self, config): DratsTCPPanel.__init__(self, config) incols = [(gobject.TYPE_STRING, "ID"), (gobject.TYPE_INT, _("Port")), (gobject.TYPE_STRING, _("Host"))] val = DratsListConfigWidget(config, "tcp_in") lw = val.add_list(incols) add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", self.but_add, lw) rem = gtk.Button(_("Remove"), gtk.STOCK_DELETE) rem.connect("clicked", self.but_rem, lw) self.mv(_("Incoming"), val, add, rem) class DratsOutEmailPanel(DratsPanel): def __init__(self, config): DratsPanel.__init__(self, config) gw = DratsConfigWidget(config, "settings", "smtp_dogw") gw.add_bool() self.mv(_("SMTP Gateway"), gw) val = DratsConfigWidget(config, "settings", "smtp_server") val.add_text() self.mv(_("SMTP Server"), val) disable_with_toggle(gw._widget, val._widget) port = DratsConfigWidget(config, "settings", "smtp_port") port.add_numeric(1, 65536, 1) mode = DratsConfigWidget(config, "settings", "smtp_tls") mode.add_bool("TLS") self.mv(_("Port and Mode"), port, mode) disable_with_toggle(gw._widget, port._widget) disable_with_toggle(gw._widget, mode._widget) val = DratsConfigWidget(config, "settings", "smtp_replyto") val.add_text() self.mv(_("Source Address"), val) disable_with_toggle(gw._widget, val._widget) val = DratsConfigWidget(config, "settings", "smtp_username") val.add_text() self.mv(_("SMTP Username"), val) disable_with_toggle(gw._widget, val._widget) val = DratsConfigWidget(config, "settings", "smtp_password") val.add_pass() self.mv(_("SMTP Password"), val) disable_with_toggle(gw._widget, val._widget) class DratsInEmailPanel(DratsPanel): INITIAL_ROWS = 2 def mv(self, title, *widgets): self.attach(widgets[0], 0, 2, 0, 1) widgets[0].show() if len(widgets) > 1: box = gtk.HBox(True, 2) for i in widgets[1:]: box.pack_start(i, 0, 0, 0) i.show() box.show() self.attach(box, 0, 2, 1, 2, yoptions=gtk.SHRINK) def but_rem(self, button, lw): lw.del_item(lw.get_selected()) def prompt_for_acct(self, fields): dlg = inputdialog.FieldDialog() for n, t, d in fields: if n in self.choices.keys(): w = miscwidgets.make_choice(self.choices[n], False, d) elif n == _("Password"): w = gtk.Entry() w.set_visibility(False) w.set_text(str(d)) elif t == bool: w = gtk.CheckButton(_("Enabled")) w.set_active(d) else: w = gtk.Entry() w.set_text(str(d)) dlg.add_field(n, w) ret = {} done = False while not done and dlg.run() == gtk.RESPONSE_OK: done = True for n, t, d in fields: try: if n in self.choices.keys(): v = dlg.get_field(n).get_active_text() elif t == bool: v = dlg.get_field(n).get_active() else: v = dlg.get_field(n).get_text() if not v: raise ValueError("empty") ret[n] = t(v) except ValueError, e: ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK) ed.set_property("text", _("Invalid value for") + " %s: %s" % (n, e)) ed.run() ed.destroy() done = False break dlg.destroy() if done: return ret else: return None def but_add(self, button, lw): fields = [(_("Server"), str, ""), (_("Username"), str, ""), (_("Password"), str, ""), (_("Poll Interval"), int, 5), (_("Use SSL"), bool, False), (_("Port"), int, 110), (_("Action"), str, "Form"), (_("Enabled"), bool, True), ] ret = self.prompt_for_acct(fields) if ret: id ="%s@%s" % (ret[_("Server")], ret[_("Username")]) lw.set_item(id, ret[_("Server")], ret[_("Username")], ret[_("Password")], ret[_("Poll Interval")], ret[_("Use SSL")], ret[_("Port")], ret[_("Action")], ret[_("Enabled")]) def but_edit(self, button, lw): vals = lw.get_item(lw.get_selected()) fields = [(_("Server"), str, vals[1]), (_("Username"), str, vals[2]), (_("Password"), str, vals[3]), (_("Poll Interval"), int, vals[4]), (_("Use SSL"), bool, vals[5]), (_("Port"), int, vals[6]), (_("Action"), str, vals[7]), (_("Enabled"), bool, vals[8]), ] id ="%s@%s" % (vals[1], vals[2]) ret = self.prompt_for_acct(fields) if ret: lw.del_item(id) id ="%s@%s" % (ret[_("Server")], ret[_("Username")]) lw.set_item(id, ret[_("Server")], ret[_("Username")], ret[_("Password")], ret[_("Poll Interval")], ret[_("Use SSL")], ret[_("Port")], ret[_("Action")], ret[_("Enabled")]) def convert_018_values(self, config, section): options = config.options(section) for opt in options: val = config.get(section, opt) if len(val.split(",")) < 7: val += ",Form" config.set(section, opt, val) print "7-8 Converted %s/%s" % (section, opt) if len(val.split(",")) < 8: val += ",True" config.set(section, opt, val) print "8-9 Converted %s/%s" % (section, opt) def __init__(self, config): DratsPanel.__init__(self, config) cols = [(gobject.TYPE_STRING, "ID"), (gobject.TYPE_STRING, _("Server")), (gobject.TYPE_STRING, _("Username")), (gobject.TYPE_STRING, _("Password")), (gobject.TYPE_INT, _("Poll Interval")), (gobject.TYPE_BOOLEAN, _("Use SSL")), (gobject.TYPE_INT, _("Port")), (gobject.TYPE_STRING, _("Action")), (gobject.TYPE_BOOLEAN, _("Enabled")) ] self.choices = { _("Action") : [_("Form"), _("Chat")], } # Remove after 0.1.9 self.convert_018_values(config, "incoming_email") val = DratsListConfigWidget(config, "incoming_email") def make_key(vals): return "%s@%s" % (vals[0], vals[1]) lw = val.add_list(cols, make_key) lw.set_password(2); add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", self.but_add, lw) edit = gtk.Button(_("Edit"), gtk.STOCK_EDIT) edit.connect("clicked", self.but_edit, lw) rem = gtk.Button(_("Remove"), gtk.STOCK_DELETE) rem.connect("clicked", self.but_rem, lw) lw.set_sort_column(1) self.mv(_("Incoming Accounts"), val, add, edit, rem) class DratsEmailAccessPanel(DratsPanel): INITIAL_ROWS = 2 def mv(self, title, *widgets): self.attach(widgets[0], 0, 2, 0, 1) widgets[0].show() if len(widgets) > 1: box = gtk.HBox(True, 2) for i in widgets[1:]: box.pack_start(i, 0, 0, 0) i.show() box.show() self.attach(box, 0, 2, 1, 2, yoptions=gtk.SHRINK) def but_rem(self, button, lw): lw.del_item(lw.get_selected()) def prompt_for_entry(self, fields): dlg = inputdialog.FieldDialog() for n, t, d in fields: if n in self.choices.keys(): w = miscwidgets.make_choice(self.choices[n], False, d) else: w = gtk.Entry() w.set_text(str(d)) dlg.add_field(n, w) ret = {} done = False while not done and dlg.run() == gtk.RESPONSE_OK: done = True for n, t, d in fields: try: if n in self.choices.keys(): v = dlg.get_field(n).get_active_text() else: v = dlg.get_field(n).get_text() if n == _("Callsign"): if not v: raise ValueError("empty") else: v = v.upper() ret[n] = t(v) except ValueError, e: ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK) ed.set_property("text", _("Invalid value for") + "%s: %s" % (n, e)) ed.run() ed.destroy() done = False break dlg.destroy() if done: return ret else: return None def but_add(self, button, lw): fields = [(_("Callsign"), str, ""), (_("Access"), str, _("Both")), (_("Email Filter"), str, "")] ret = self.prompt_for_entry(fields) if ret: id = "%s/%i" % (ret[_("Callsign")], random.randint(1, 1000)) lw.set_item(id, ret[_("Callsign")], ret[_("Access")], ret[_("Email Filter")]) def but_edit(self, button, lw): vals = lw.get_item(lw.get_selected()) if not vals: return fields = [(_("Callsign"), str, vals[1]), (_("Access"), str, vals[2]), (_("Email Filter"), str, vals[3])] id = vals[0] ret = self.prompt_for_entry(fields) if ret: lw.del_item(id) lw.set_item(id, ret[_("Callsign")], ret[_("Access")], ret[_("Email Filter")]) def __init__(self, config): DratsPanel.__init__(self, config) cols = [(gobject.TYPE_STRING, "ID"), (gobject.TYPE_STRING, _("Callsign")), (gobject.TYPE_STRING, _("Access")), (gobject.TYPE_STRING, _("Email Filter"))] self.choices = { _("Access") : [_("None"), _("Both"), _("Incoming"), _("Outgoing")], } val = DratsListConfigWidget(config, "email_access") def make_key(vals): return "%s/%i" % (vals[0], random.randint(0, 1000)) lw = val.add_list(cols, make_key) add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", self.but_add, lw) edit = gtk.Button(_("Edit"), gtk.STOCK_EDIT) edit.connect("clicked", self.but_edit, lw) rem = gtk.Button(_("Remove"), gtk.STOCK_DELETE) rem.connect("clicked", self.but_rem, lw) lw.set_sort_column(1) self.mv(_("Email Access"), val, add, edit, rem) class DratsConfigUI(gtk.Dialog): def mouse_event(self, view, event): x, y = event.get_coords() path = view.get_path_at_pos(int(x), int(y)) if path: view.set_cursor_on_cell(path[0]) try: (store, iter) = view.get_selection().get_selected() selected, = store.get(iter, 0) except Exception, e: print "Unable to find selected: %s" % e return None for v in self.panels.values(): v.hide() self.panels[selected].show() def move_cursor(self, view, step, count): try: (store, iter) = view.get_selection().get_selected() selected, = store.get(iter, 0) except Exception, e: print "Unable to find selected: %s" % e return None for v in self.panels.values(): v.hide() self.panels[selected].show() def build_ui(self): hbox = gtk.HBox(False, 2) self.__store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) self.__tree = gtk.TreeView(self.__store) hbox.pack_start(self.__tree, 0, 0, 0) self.__tree.set_size_request(150, -1) self.__tree.set_headers_visible(False) rend = gtk.CellRendererText() col = gtk.TreeViewColumn(None, rend, text=1) self.__tree.append_column(col) self.__tree.show() self.__tree.connect("button_press_event", self.mouse_event) self.__tree.connect_after("move-cursor", self.move_cursor) def add_panel(c, s, l, par, *args): p = c(self.config, *args) p.show() sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add_with_viewport(p) hbox.pack_start(sw, 1, 1, 1) self.panels[s] = sw for val in p.vals: self.tips.set_tip(val, config_tips.get_tip(val.vsec, val.vname)) return self.__store.append(par, row=(s, l)) prefs = add_panel(DratsPrefsPanel, "prefs", _("Preferences"), None) add_panel(DratsPathsPanel, "paths", _("Paths"), prefs) add_panel(DratsGPSPanel, "gps", _("GPS"), prefs, self) add_panel(DratsAppearancePanel, "appearance", _("Appearance"), prefs) add_panel(DratsChatPanel, "chat", _("Chat"), prefs) add_panel(DratsSoundPanel, "sounds", _("Sounds"), prefs) add_panel(DratsMessagePanel, "messages", _("Messages"), None) radio = add_panel(DratsRadioPanel, "radio", _("Radio"), None) add_panel(DratsTransfersPanel, "transfers", _("Transfers"), radio) network = add_panel(DratsNetworkPanel, "network", _("Network"), None) add_panel(DratsTCPIncomingPanel, "tcpin", _("TCP Gateway"), network) add_panel(DratsTCPOutgoingPanel, "tcpout", _("TCP Forwarding"), network) add_panel(DratsOutEmailPanel, "smtp", _("Outgoing Email"), network) add_panel(DratsInEmailPanel, "email", _("Email Accounts"), network) add_panel(DratsEmailAccessPanel, "email_ac", _("Email Access"), network) self.panels["prefs"].show() hbox.show() self.vbox.pack_start(hbox, 1, 1, 1) self.__tree.expand_all() def save(self): for widget in self.config.widgets: widget.save() def __init__(self, config, parent=None): gtk.Dialog.__init__(self, title=_("Config"), buttons=(gtk.STOCK_SAVE, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), parent=parent) self.config = config self.panels = {} self.tips = gtk.Tooltips() self.build_ui() self.set_default_size(600, 400) class DratsConfig(ConfigParser.ConfigParser): def set_defaults(self): for sec, opts in DEFAULTS.items(): if not self.has_section(sec): self.add_section(sec) for opt, value in opts.items(): if not self.has_option(sec, opt): self.set(sec, opt, value) def __init__(self, mainapp, safe=False): ConfigParser.ConfigParser.__init__(self) self.platform = platform.get_platform() self.filename = self.platform.config_file("d-rats.config") print "FILE: %s" % self.filename self.read(self.filename) self.widgets = [] self.set_defaults() if self.get("prefs", "download_dir") == ".": default_dir = os.path.join(platform.get_platform().default_dir(), "D-RATS Shared") if not os.path.exists(default_dir): print "Creating downlaod directory: %s" % default_dir os.mkdir(default_dir) self.set("prefs", "download_dir", default_dir) def show(self, parent=None): ui = DratsConfigUI(self, parent) r = ui.run() if r == gtk.RESPONSE_OK: ui.save() self.save() ui.destroy() return r == gtk.RESPONSE_OK def save(self): f = file(self.filename, "w") self.write(f) f.close() def getboolean(self, sec, key): try: return ConfigParser.ConfigParser.getboolean(self, sec, key) except: print "Failed to get boolean: %s/%s" % (sec, key) return False def getint(self, sec, key): return int(float(ConfigParser.ConfigParser.get(self, sec, key))) def form_source_dir(self): d = os.path.join(self.platform.config_dir(), "Form_Templates") if not os.path.isdir(d): os.mkdir(d) return d def form_store_dir(self): d = os.path.join(self.platform.config_dir(), "messages") if not os.path.isdir(d): os.mkdir(d) return d def ship_obj_fn(self, name): return os.path.join(self.platform.source_dir(), name) def ship_img(self, name): path = self.ship_obj_fn(os.path.join("images", name)) return gtk.gdk.pixbuf_new_from_file(path) if __name__ == "__main__": fn = "/home/dan/.d-rats/d-rats.config" cf = ConfigParser.ConfigParser() cf.read(fn) cf.widgets = [] c = DratsConfigUI(cf) if c.run() == gtk.RESPONSE_OK: c.save(fn) d-rats-0.3.3/d_rats/config_tips.py000066400000000000000000000103641160617671700170610ustar00rootroot00000000000000#!/usr/bin/python TIPS_USER = { "latitude" : _("Your current latitude. Use decimal degrees (DD.DDDDD)\nor D*M'S\". Use a space for special characters").replace("*", u"\u00B0"), "longitude" : _("Your current longitude. Use decimal degrees (DD.DDDDD)\nor D*M'S\". Use a space for special characters").replace("*", u"\u00B0"), "altitude" : _("Your current altitude"), } TIPS_PREFS = { "useutc" : _("When enabled, form time fields will default to current time in UTC. When disabled, default to local time"), "language" : _("Requires a D-RATS restart"), "allow_remote_forms" : _("Allow remote stations to pull forms"), "allow_remote_files" : _("Allow remote stations to pull files"), "form_default_private" : _("Default state for private flag on new forms"), "msg_include_reply" : _("Include the text of the original message when replying (not recommended as it wastes bandwidth)"), } TIPS_SETTINGS = { "port" : _("On Windows, use something like 'COM12'") + "\n" + \ _("On UNIX, use something like '/dev/ttyUSB0'") + "\n" + \ _("For a network connection, use something like 'net:host:9000'"), "rate" : _("9600 for mobile radios, 38400 for handhelds"), "gpsport" : _("Serial port for an NMEA-compliant external GPS"), "gpsenabled" : _("If enabled, take current position from the external GPS"), "gpsportspeed" : _("The NMEA standard is 4800"), "aprssymtab" : _("The symbol table character for GPS-A beacons"), "aprssymbol" : _("The symbol character for GPS-A beacons"), "compatmode" : _("Treat incoming raw text (and garbage) as chat data and display it on-screen"), "mapdir" : _("Alternate location to store cached map images"), "warmup_length" : _("Amount of fake data to send during a warmup cycle"), "warmup_timeout" : _("Length of time between transmissions that must pass before we send a warmup block to open the power-save circuits on handhelds"), "force_delay" : _("Amount of time to wait between transmissions in seconds (a positive number is a fixed delay, a negative value means 'randomly choose between 0 and X')"), "delete_from" : _("Comma-separated list of callsigns that may delete files remotely"), "remote_admin_passwd" : _("Password required for remote administration tasks (blank for none)"), "ping_info" : _("Text string to return in response to a ping.") + "\n" + \ _("If prefixed by a > character, interpret as a path to a text file") + "\n" + \ _("If prefixed by a ! character, interpret as a path to a script"), "smtp_server" : _("Hostname of outgoing SMTP server. If this is specified, this station will be a gateway for email forms. If left blank, this feature is disabled"), "smtp_replyto" : _("Email address to set on outgoing form email messages"), "smtp_tls" : _("If enabled, attempt to negotiate TLS/SSL with SMTP server"), "smtp_username" : _("Username for SMTP authentication. Disabled if blank"), "smtp_password" : _("Password for SMTP authentication"), "smtp_port" : _("Default is 25. Set to the value given by your ISP"), "sniff_packets" : _("Display information about packets seen that are destined for other stations"), "map_tile_ttl" : _("After this many hours, a map tile will be re-fetched, regardless of if it is already stored locally. 0 means never re-fetch. 720 hours is 30 days."), "msg_flush" : _("Seconds between each attempt to process forwarded messages. Do not set this too low!"), "station_msg_ttl" : _("If a station was last heard more than this many seconds ago, do not assume you have a clear path (ping it first)"), "timestamp_positions" : _("For each position report recieved, change the callsign to 'callsign.datestamp'. NOTE: This will generate a LOT of map pointers, use with caution!"), } sb = _("Specify a .WAV file to be played") TIPS_SOUNDS = { "chat" : sb + _(" when a new chat arrives"), "messages" : sb + _(" when new message activity occurs"), "files" : sb + _(" when new file activity occurs"), } CONFIG_TIPS = { "user" : TIPS_USER, "prefs" : TIPS_PREFS, "settings" : TIPS_SETTINGS, "sounds" : TIPS_SOUNDS, } def get_tip(section, value): try: tip = CONFIG_TIPS[section][value] except KeyError: tip = None return tip d-rats-0.3.3/d_rats/ddt2.py000066400000000000000000000160621160617671700154130ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import struct import zlib import base64 import yencode import threading import utils ENCODED_HEADER = "[SOB]" ENCODED_TRAILER = "[EOB]" def update_crc(c, crc): for _ in range(0,8): c <<= 1 if (c & 0400) != 0: v = 1 else: v = 0 if (crc & 0x8000): crc <<= 1 crc += v crc ^= 0x1021 else: crc <<= 1 crc += v return crc & 0xFFFF def calc_checksum(data): checksum = 0 for i in data: checksum = update_crc(ord(i), checksum) checksum = update_crc(0, checksum) checksum = update_crc(0, checksum) return checksum def encode(data): return yencode.yencode_buffer(data) def decode(data): return yencode.ydecode_buffer(data) class DDT2Frame(object): format = "!BHBBHH8s8s" cso = 6 csl = 2 def __init__(self): self.seq = 0 self.session = 0 self.type = 0 self.d_station = "" self.s_station = "" self.data = "" self.magic = 0xDD self.sent_event = threading.Event() self.ackd_event = threading.Event() self.compress = True self._xmit_s = 0 self._xmit_e = 0 self._xmit_z = 0 def get_xmit_bps(self): if not self._xmit_e: print "Block not sent, can't determine BPS!" return 0 if self._xmit_s == self._xmit_e: return self._xmit_z * 100 # Fudge for sockets return self._xmit_z / (self._xmit_e - self._xmit_s) def set_compress(self, compress=True): self.compress = compress def get_packed(self): if self.compress: data = zlib.compress(self.data, 9) else: data = self.data self.magic = (~self.magic) & 0xFF length = len(data) s_station = self.s_station.ljust(8, "~") d_station = self.d_station.ljust(8, "~") val = struct.pack(self.format, self.magic, self.seq, self.session, self.type, 0, length, s_station, d_station) checksum = calc_checksum(val + data) val = struct.pack(self.format, self.magic, self.seq, self.session, self.type, checksum, length, s_station, d_station) self._xmit_z = len(val) + len(data) return val + data def unpack(self, val): magic = ord(val[0]) if magic == 0xDD: self.compress = True elif magic == 0x22: self.compress = False else: print "Magic 0x%X not recognized" % magic return False header = val[:25] data = val[25:] (magic, self.seq, self.session, self.type, checksum, length, self.s_station, self.d_station) = struct.unpack(self.format, header) _header = struct.pack(self.format, magic, self.seq, self.session, self.type, 0, length, self.s_station, self.d_station) _checksum = calc_checksum(_header + data) self.s_station = self.s_station.replace("~", "") self.d_station = self.d_station.replace("~", "") if _checksum != checksum: print "Checksum failed: %s != %s" % (checksum, _checksum) return False if self.compress: self.data = zlib.decompress(data) else: self.data = data return True def __str__(self): if self.compress: c = "+" else: c = "-" data = utils.filter_to_ascii(self.data[:20]) return "DDT2%s: %i:%i:%i %s->%s (%s...[%i])" % (c, self.seq, self.session, self.type, self.s_station, self.d_station, data, len(self.data)) def get_copy(self): f = self.__class__() f.seq = self.seq f.session = self.session f.type = self.type f.s_station = self.s_station f.d_station = self.d_station f.data = self.data f.set_compress(self.compress) return f class DDT2EncodedFrame(DDT2Frame): def get_packed(self): raw = DDT2Frame.get_packed(self) encoded = encode(raw) return ENCODED_HEADER + encoded + ENCODED_TRAILER def unpack(self, val): try: h = val.index(ENCODED_HEADER) + len(ENCODED_TRAILER) t = val.rindex(ENCODED_TRAILER) payload = val[h:t] except Exception, e: print "Block has no header/trailer: %s" % e return False try: decoded = decode(payload) except Exception, e: print "Unable to decode frame: %s" % e return False return DDT2Frame.unpack(self, decoded) class DDT2RawData(DDT2Frame): def get_packed(self): return self.data def unpack(self, string): return self.data def test_symmetric(compress=True): fin = DDT2EncodedFrame() fin.type = 1 fin.session = 2 fin.seq = 3 fin.s_station = "FOO" fin.d_station = "BAR" fin.data = "This is a test" fin.set_compress(compress) p = fin.get_packed() print p fout = DDT2EncodedFrame() fout.unpack(p) #print fout.__dict__ print fout def test_crap(): f = DDT2EncodedFrame() try: if f.unpack("[SOB]foobar[EOB]"): print "FAIL" else: print "PASS" except Exception, e: print "PASS" if __name__ == "__main__": test_symmetric() test_symmetric(False) test_crap() d-rats-0.3.3/d_rats/emailgw.py000066400000000000000000000264521160617671700162070ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import threading import poplib import smtplib import email try: from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText except ImportError: # Python 2.4 from email import MIMEMultipart from email import MIMEBase from email import MIMEText import rfc822 import time import platform import gobject import re import random import formgui from ui import main_events import signals import utils import msgrouting def create_form_from_mail(config, mail, tmpfn): subject = mail.get("Subject", "[no subject]") sender = mail.get("From", "Unknown ") xml = None body = "" if mail.is_multipart(): html = None for part in mail.walk(): if part.get_content_maintype() == "multipart": continue elif part.get_content_type() == "d-rats/form_xml": xml = str(part.get_payload()) break # A form payload trumps all elif part.get_content_type() == "text/plain": body += part.get_payload(decode=True) elif part.get_content_type() == "text/html": html = part.get_payload(decode=True) if not body: body = html else: body = mail.get_payload(decode=True) if not body and not xml: raise Exception("Unable to find a usable part") messageid = mail.get("Message-ID", time.strftime("%m%d%Y%H%M%S")) if not msgrouting.msg_lock(tmpfn): print "AIEE: Unable to lock incoming email message file!" if xml: f = file(tmpfn, "w") f.write(xml) f.close() form = formgui.FormFile(tmpfn) recip = form.get_recipient_string() if "%" in recip: recip, addr = recip.split("%", 1) recip = recip.upper() else: print "Email from %s: %s" % (sender, subject) recip, addr = rfc822.parseaddr(mail.get("To", "UNKNOWN")) efn = os.path.join(config.form_source_dir(), "email.xml") form = formgui.FormFile(efn) form.set_field_value("_auto_sender", sender) form.set_field_value("recipient", recip) form.set_field_value("subject", "EMAIL: %s" % subject) form.set_field_value("message", utils.filter_to_ascii(body)) form.set_path_src(sender.strip()) form.set_path_dst(recip.strip()) form.set_path_mid(messageid) form.save_to(tmpfn) return form class MailThread(threading.Thread, gobject.GObject): __gsignals__ = { "user-send-chat" : signals.USER_SEND_CHAT, "user-send-form" : signals.USER_SEND_FORM, "form-received" : signals.FORM_RECEIVED, "get-station-list" : signals.GET_STATION_LIST, "event" : signals.EVENT, "mail-thread-complete" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN, gobject.TYPE_STRING)), } _signals = __gsignals__ def _emit(self, signal, *args): gobject.idle_add(self.emit, signal, *args) def __init__(self, config, host, user, pasw, port=110, ssl=False): threading.Thread.__init__(self) gobject.GObject.__init__(self) self.setDaemon(True) self.username = user self.password = pasw self.server = host self.port = port self.use_ssl = ssl self.config = config self._coerce_call = None def message(self, message): print "[MAIL %s@%s] %s" % (self.username, self.server, message) def create_form_from_mail(self, mail): id = self.config.get("user", "callsign") + \ time.strftime("%m%d%Y%H%M%S") + \ mail.get("Message-id", str(random.randint(0, 1000))) mid = platform.get_platform().filter_filename(id) ffn = os.path.join(self.config.form_store_dir(), _("Inbox"), "%s.xml" % mid) try: form = create_form_from_mail(self.config, mail, ffn) except Exception, e: print "Failed to create form from mail: %s" % e return if self._coerce_call: print "Coercing to %s" % self._coerce_call form.set_path_dst(self._coerce_call) else: print "Not coercing" form.add_path_element("EMAIL") form.add_path_element(self.config.get("user", "callsign")) form.save_to(ffn) self._emit("form-received", -999, ffn) def fetch_mails(self): self.message("Querying %s:%i" % (self.server, self.port)) if self.use_ssl: server = poplib.POP3_SSL(self.server, self.port) else: server = poplib.POP3(self.server, self.port) server.user(self.username) server.pass_(self.password) num = len(server.list()[1]) messages = [] for i in range(num): self.message("Fetching %i/%i" % (i+1, num)) result = server.retr(i+1) server.dele(i+1) message = email.message_from_string("\r\n".join(result[1])) messages.append(message) server.quit() return messages def run(self): self.message("One-shot thread starting") mails = None if not self.config.getboolean("state", "connected_inet"): result = "Not connected to the Internet" else: try: mails = self.fetch_mails() except Exception, e: result = "Failed (%s)" % e if mails: for mail in mails: self.create_form_from_mail(mail) event = main_events.Event(_("Received %i messages") % \ len(mails)) self._emit("event", event) result = "Queued %i messages" % len(mails) elif mails is not None: result = "No messages" self.message("Thread ended [ %s ]" % result) self._emit("mail-thread-complete", mails != None, result) class CoercedMailThread(MailThread): def __init__(self, *args): call = str(args[-1]) args = args[:-1] MailThread.__init__(self, *args) self._coerce_call = call class AccountMailThread(MailThread): def __init__(self, config, account): settings = config.get("incoming_email", account) try: host, user, pasw, poll, ssl, port, action, enb = \ settings.split(",", 7) except ValueError: raise Exception("Unable to parse account settings for `%s'" % \ account) actions = { _("Form") : self.create_form_from_mail, _("Chat") : self.do_chat_from_mail, } try: self.__action = actions[action] except KeyError: raise Exception("Unsupported action `%s' for %s@%s" % \ (action, user, host)) ssl = ssl == "True" if not port: port = ssl and 995 or 110 else: port = int(port) self._poll = int(poll) self.event = threading.Event() self.enabled = enb == "True" MailThread.__init__(self, config, host, user, pasw, port, ssl) def do_chat_from_mail(self, mail): if mail.is_multipart(): body = None for part in mail.walk(): html = None if part.get_content_type() == "text/plain": body = part.get_payload(decode=True) break elif part.get_content_type() == "text/html": html = part.get_payload(decode=True) if not body: body = html else: body = mail.get_payload() text = "Message from %s (%s):\r\n%s" % (\ mail.get("From", "Unknown Sender"), mail.get("Subject", ""), body) for port in self.emit("get-station-list").keys(): self._emit("user-send-chat", "CQCQCQ", port, text, False) event = main_events.Event(None, "Mail received from %s and sent via chat" % \ mail.get("From", "Unknown Sender")) self._emit("event", event) def run(self): if not self.config.getboolean("state", "connected_inet"): self.message("Not connected") else: mails = [] try: mails = self.fetch_mails() except Exception, e: self.message("Failed to retrieve messages: %s" % e) for mail in mails: self.__action(mail) if mails: event = main_events.Event(None, "Received %i email(s)" % len(mails)) self._emit("event", event) class PeriodicAccountMailThread(AccountMailThread): def run(self): self.message("Periodic thread starting") while self.enabled: AccountMailThread.run(self) self.event.wait(self._poll * 60) self.event.clear() self.message("Thread ending") def trigger(self): self.event.set() def stop(self): self.enabled = False self.trigger() def __validate_access(config, callsign, emailaddr, types): rules = config.options("email_access") for rule in rules: rulespec = config.get("email_access", rule) call, access, filter = rulespec.split(",", 2) if call in [callsign, "*"] and re.search(filter, emailaddr): #print "%s -> %s matches %s,%s,%s" % (callsign, emailaddr, # call, access, filter) #print "Access types allowed: %s" % types return access in types #else: #print "%s -> %s does not match %s,%s,%s" % (callsign, emailaddr, # call, access, filter) print "No match found" return False def validate_outgoing(config, callsign, emailaddr): return __validate_access(config, callsign, emailaddr, ["Both", "Outgoing"]) def validate_incoming(config, callsign, emailaddr): return __validate_access(config, callsign, emailaddr, ["Both", "Incoming"]) if __name__ == "__main__": class fakeout(object): form_source_dir = "forms" form_store_dir = "." def reg_form(self, *args): pass def list_add_form(self, *args, **kwargs): pass def get_stamp(self): return "FOO" mt = MailThread(None, fakeout()) mt.run() d-rats-0.3.3/d_rats/formbuilder.py000066400000000000000000000510441160617671700170670ustar00rootroot00000000000000# # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject import os import glob import tempfile import shutil from miscwidgets import make_choice from formgui import FormDialog,FormFile,xml_escape,xml_unescape import formgui import mainapp from d_rats import platform class FormElementEditor(gtk.Dialog): def make_entry_editor(self, id): entry = gtk.Entry() entry.show() f = gtk.Frame("Initial value:") f.add(entry) self.entries[id] = entry return f def make_null_editor(self, id): return gtk.Label("(There are no options for this type)") def make_toggle_editor(self, id): cb = gtk.CheckButton("True") cb.show() f = gtk.Frame("Default value") f.add(cb) self.entries[id] = cb return f def make_choice_editor(self, id, single=True): self._choice_buffer = gtk.TextBuffer() entry = gtk.TextView(self._choice_buffer) entry.show() sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.add(entry) sw.show() if single: f = gtk.Frame("Options (one per line, first is default)") else: f = gtk.Frame("Options (one per line)") f.add(sw) self.entries[id] = entry return f def type_changed(self, box, data=None): sel = box.get_active_text() print "Selected: %s" % sel for t,w in self.vals.items(): if t == sel: w.show() else: w.hide() def __init__(self): gtk.Dialog.__init__(self, title="Edit form element", buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) self.entries = {} self.set_default_size(150,150) self.vals = { "text" : self.make_entry_editor("text"), "multiline" : self.make_entry_editor("multiline"), "date" : self.make_null_editor("date"), "time" : self.make_null_editor("time"), "numeric" : self.make_entry_editor("numeric"), "toggle" : self.make_toggle_editor("toggle"), "choice" : self.make_choice_editor("choice"), "multiselect": self.make_choice_editor("multiselect", False), "label" : self.make_null_editor("label"), } self.type_sel = make_choice(self.vals.keys(), False, "text") self.type_sel.connect("changed", self.type_changed, None) self.type_sel.show() self.vals["text"].show() self.ts_frame = gtk.Frame("Field Type") self.ts_frame.show() self.ts_frame.add(self.type_sel) self.vbox.pack_start(self.ts_frame, 0,0,0) for t,w in self.vals.items(): self.vbox.pack_start(w, 1,1,1) def get_initial_value(self): sel = self.type_sel.get_active_text() if sel in ("text", "multiline", "numeric"): return self.entries[sel].get_text() elif sel in ("choice",): b = self.entries[sel].get_buffer() i = b.get_iter_at_line(1) i.backward_chars(1) return b.get_text(b.get_start_iter(), i) elif sel in ("toggle"): return str(self.entries[sel].get_active()) else: return "" def set_initial_value(self, val): sel = self.type_sel.get_active_text() if sel in ("text", "multiline", "numeric"): return self.entries[sel].set_text(val) elif sel in ("toggle"): try: b = eval(val) except: b = False self.entries[sel].set_active(b) def get_options(self): sel = self.type_sel.get_active_text() if sel == "choice": b = self.entries[sel].get_buffer() t = b.get_text(b.get_start_iter(), b.get_end_iter()) return str(t.split("\n")) elif sel == "multiselect": b = self.entries[sel].get_buffer() t = b.get_text(b.get_start_iter(), b.get_end_iter()) opts = t.split("\n") return str([(False, x) for x in opts]) else: return "" def set_options(self, val): sel = self.type_sel.get_active_text() if sel == "choice": try: l = eval(val) except: return b = self.entries[sel].get_buffer() b.set_text("\n".join(l)) elif sel == "multiselect": try: l = eval(val) except: return b = self.entries[sel].get_buffer() b.set_text("\n".join([y for x,y in l])) def get_type(self): return self.type_sel.get_active_text() def set_type(self, type): self.type_sel.set_active(self.vals.keys().index(type)) class FormBuilderGUI(gtk.Dialog): def reorder(self, up): try: (list, iter) = self.view.get_selection().get_selected() pos = int(list.get_path(iter)[0]) if up: target = list.get_iter(pos - 1) else: target = list.get_iter(pos + 1) if target: list.swap(iter, target) except: return def but_move_up(self, widget, data=None): self.reorder(True) def but_move_down(self, widget, data=None): self.reorder(False) def but_add(self, widget, data=None): d = FormElementEditor() r = d.run() if r == gtk.RESPONSE_CANCEL: d.destroy() return iv = d.get_initial_value() print "Type: %s" % d.get_type() print "Initial: %s" % iv print "Opts: %s" % d.get_options() iter = self.store.append() self.store.set(iter, self.col_id, "foo", self.col_type, d.get_type(), self.col_cap, "Untitled", self.col_value, iv, self.col_opts, d.get_options(), self.col_inst, "") d.destroy() def but_delete(self, widget, data=None): try: (list, iter) = self.view.get_selection().get_selected() list.remove(iter) except: return def but_edit(self, widget, data=None): try: (list, iter) = self.view.get_selection().get_selected() (t, v, o) = list.get(iter, self.col_type, self.col_value, self.col_opts) except: return d = FormElementEditor() d.set_type(t) d.set_initial_value(v) d.set_options(o) r = d.run() if r == gtk.RESPONSE_OK: list.set(iter, self.col_type, d.get_type(), self.col_value, d.get_initial_value(), self.col_opts, d.get_options()) d.destroy() def ev_edited(self, r, path, new_text, colnum): iter = self.store.get_iter(path) self.store.set(iter, colnum, new_text) def build_display(self): self.col_id = 0 self.col_type = 1 self.col_cap = 2 self.col_value = 3 self.col_opts = 4 self.col_inst = 5 self.store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) self.view = gtk.TreeView(self.store) self.view.set_rules_hint(True) self.view.show() l = [(self.col_id, "ID", True), (self.col_type, "Type", False), (self.col_cap, "Caption", True), (self.col_value, "Initial Value", False)] for i in l: (col, cap, ed) = i r = gtk.CellRendererText() r.set_property("editable", ed) if ed: r.connect("edited", self.ev_edited, col) c = gtk.TreeViewColumn(cap, r, text=col) c.set_resizable(True) c.set_sort_column_id(col) self.view.append_column(c) sw = gtk.ScrolledWindow() sw.add(self.view) sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.show() return sw def make_field_xml(self, model, path, iter, _): (id, type, cap, val, opts, inst) = model.get(iter, self.col_id, self.col_type, self.col_cap, self.col_value, self.col_opts, self.col_inst) if val: val = xml_escape(val) print "\n\nField type: %s" % type cap_xml = "%s" % cap if type not in ["choice", "multiselect"] and val: ent_xml = "%s" % (type, val) elif type == "choice": try: print "Opts: %s" % opts l = eval(opts) ent_xml = "" % type for c in l: if c == val: set = " set='y'" else: set = "" ent_xml += "%s" % (set, c) ent_xml += "" except Exception, e: print "Exception parsing choice list: %s" % e ent_xml = "" % opts elif type == "multiselect": try: l = eval(opts) ent_xml = "" % type for v, c in l: setval = v and "y" or "n" ent_xml += "%s" % (setval, c) ent_xml += "" except Exception, e: print "Exception parsing choice list: %s" % e ent_xml = "" % opts else: ent_xml = "" % type field_xml = "\n%s\n%s\n\n" % (id, cap_xml, ent_xml) print "Field XML: %s\n\n" % field_xml self.xml += field_xml def get_form_xml(self): id = self.props["ID"].get_text() title = self.props["Title"].get_text() logo = self.props["Logo"].get_active_text() self.xml = "\n
\n%s\n" % (id,title) if logo: self.xml += "%s" % logo self.store.foreach(self.make_field_xml, None) self.xml += "
\n
\n" return self.xml def build_buttons(self): box = gtk.VBox(True, 2) l = [("Move Up", self.but_move_up), ("Add", self.but_add), ("Edit", self.but_edit), ("Delete", self.but_delete), ("Move Down", self.but_move_down), ] for i in l: (cap, func) = i b = gtk.Button(cap) b.connect("clicked", func, None) box.pack_start(b, 0,0,0) b.show() box.show() return box def make_field(self, caption, choices=None): box = gtk.HBox(False, 2) l = gtk.Label(caption) l.set_size_request(45, -1) l.show() if choices is not None: e = make_choice(choices, True) else: e = gtk.Entry() e.show() self.props[caption] = e box.pack_start(l, 0,0,0) box.pack_start(e, 1,1,1) box.show() return box def build_formprops(self): self.props = {} frame = gtk.Frame("Form Properties") path = mainapp.get_mainapp().config.get("settings", "form_logo_dir") logos = [] for fn in glob.glob(os.path.join(path, "*.*")): logos.append(fn.replace(path, "")[1:]) box = gtk.VBox(False, 2) for i in ["Title", "ID"]: f = self.make_field(i) box.pack_start(f, 0,0,0) f.show() f = self.make_field("Logo", logos) box.pack_start(f, 0, 0, 0) f.show() box.show() frame.add(box) frame.show() return frame def build_fieldeditor(self): frame = gtk.Frame("Form Elements") box = gtk.HBox(False, 2) box.pack_start(self.build_display(), 1,1,1) box.pack_start(self.build_buttons(), 0,0,0) box.show() frame.add(box) frame.show() return frame def show_preview(self, widget, data=None): fd, n = tempfile.mkstemp() f = file(n, "w") f.write(self.get_form_xml()) f.close() os.close(fd) d = FormDialog("Preview of form", n, parent=self) config = mainapp.get_mainapp().config d.configure(config) d.run() d.destroy() os.remove(n) def load_field(self, widget): iter = self.store.append() print "Type: %s" % widget.type if widget.type in ["choice", "multiselect"]: opts = widget.choices print "Opts for %s: %s" % (widget.type, opts) else: opts = None self.store.set(iter, self.col_id, widget.id, self.col_type, widget.type, self.col_cap, widget.caption, self.col_value, widget.get_value(), self.col_opts, opts) def load_from_file(self, filename): form = FormDialog("", filename) self.props["ID"].set_text(form.id) self.props["Title"].set_text(form.title_text) self.props["Logo"].child.set_text(form.logo_path or "") for f in form.fields: w = f.entry self.load_field(w) del form def __init__(self): gtk.Dialog.__init__(self, title="Form builder", buttons=(gtk.STOCK_SAVE, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) self.vbox.pack_start(self.build_formprops(), 0,0,0) self.vbox.pack_start(self.build_fieldeditor(), 1,1,1) preview = gtk.Button("Preview") preview.connect("clicked", self.show_preview, None) preview.show() self.action_area.pack_start(preview, 0,0,0) class FormManagerGUI(object): def add_form(self, filename): try: form = FormFile(filename) id = form.id title = form.title_text del form except Exception, e: import utils utils.log_exception() id = "broken" title = "Broken Form - Delete me" iter = self.store.get_iter_first() while iter: form_id, = self.store.get(iter, self.col_id) print "Checking %s against %s" % (form_id, id) if form_id == id: raise Exception("Cannot add duplicate form `%s'" % form_id) iter = self.store.iter_next(iter) iter = self.store.append() self.store.set(iter, self.col_id, id, self.col_title, title, self.col_file, filename) return id def but_new(self, widget, data=None): d = FormBuilderGUI() r = d.run() if r != gtk.RESPONSE_CANCEL: id = d.props["ID"].get_text() xml = d.get_form_xml() f = file(os.path.join(self.dir, "%s.xml" % id), "w") f.write(xml) f.close() self.add_form(f.name) d.destroy() def but_edit(self, widget, data=None): try: (list, iter) = self.view.get_selection().get_selected() (filename, _id) = list.get(iter, self.col_file, self.col_id) except: return d = FormBuilderGUI() d.load_from_file(filename) r = d.run() if r != gtk.RESPONSE_CANCEL: id = d.props["ID"].get_text() xml = d.get_form_xml() f = file(os.path.join(self.dir, "%s.xml" % id), "w") f.write(xml) f.close() if id != _id: # FIXME: Delete old file self.add_form(f.name) d.destroy() def but_delete(self, widget, data=None): try: (list, iter) = self.view.get_selection().get_selected() (file, ) = list.get(iter, self.col_file) list.remove(iter) os.remove(file) except: return def but_close(self, widget, data=None): self.window.destroy() def but_import(self, widget, data=None): p = platform.get_platform() fn = p.gui_open_file() if not fn: return try: form_id = self.add_form(fn) except Exception, e: d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK) d.set_markup("Unable to add form") d.format_secondary_text(str(e)) d.run() d.destroy() shutil.copy(fn, os.path.join(self.dir, "%s.xml" % form_id)) def but_export(self, widget, data=None): try: (list, iter) = self.view.get_selection().get_selected() (filename, _id) = list.get(iter, self.col_file, self.col_id) except: return p = platform.get_platform() fn = p.gui_save_file(default_name="%s.xml" % _id) if fn: shutil.copy(filename, fn) def make_list(self): self.col_id = 0 self.col_title = 1 self.col_file = 2 self.store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) self.view = gtk.TreeView(self.store) self.view.set_rules_hint(True) self.view.show() l = [(self.col_id, "ID"), (self.col_title, "Title")] for col,cap in l: r = gtk.CellRendererText() c = gtk.TreeViewColumn(cap, r, text=col) c.set_sort_column_id(col) self.view.append_column(c) sw = gtk.ScrolledWindow() sw.add(self.view) sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.show() return sw def make_buttons(self): l = [("New", self.but_new), ("Edit", self.but_edit), ("Delete", self.but_delete), ("Close", self.but_close), ("Import", self.but_import), ("Export", self.but_export), ] hbox = gtk.HBox(True, 2) for cap,func in l: b = gtk.Button(cap) b.connect("clicked", func, None) b.show() hbox.add(b) hbox.show() return hbox def __init__(self, dir): self.dir = dir vbox = gtk.VBox(False, 2) vbox.pack_start(self.make_list(), 1,1,1) vbox.pack_start(self.make_buttons(), 0,0,0) files = glob.glob(os.path.join(dir, "*.xml")) for f in files: self.add_form(f) vbox.show() self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title("Form Manager") self.window.set_default_size(275,300) self.window.add(vbox) self.window.show() if __name__=="__main__": m = FormManagerGUI("Form_Templates") gtk.main() d-rats-0.3.3/d_rats/formgui.py000066400000000000000000001074541160617671700162340ustar00rootroot00000000000000# # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import time import os import tempfile import zlib import base64 import libxml2 import libxslt import gtk import gobject import pango from miscwidgets import make_choice, KeyedListWidget from utils import run_or_error from ui.main_common import ask_for_confirmation import platform import spell test = """
Test Form Name Foobar Is this person okay? True
""" xml_escapes = [("<", "<"), (">", ">"), ("&", "&"), ('"', """), ("'", "'")] RESPONSE_SEND = -900 RESPONSE_SAVE = -901 RESPONSE_REPLY = -902 RESPONSE_DELETE = -903 RESPONSE_SEND_VIA = -904 style = gtk.Style() for i in [style.fg, style.bg, style.base]: i[gtk.STATE_INSENSITIVE] = i[gtk.STATE_NORMAL] STYLE_BRIGHT_INSENSITIVE = style del style del i def xml_escape(string): d = {} for char, esc in xml_escapes: d[char] = esc out = "" for i in string: out += d.get(i, i) return out def xml_unescape(string): d = {} for char, esc in xml_escapes: d[esc] = char out = "" i = 0 while i < len(string): if string[i] != "&": out += string[i] i += 1 else: try: semi = string[i:].index(";") + i + 1 except: print "XML Error: & with no ;" i += 1 continue esc = string[i:semi] if not esc: print "No escape: %i:%i" % (i, semi) i += 1 continue if d.has_key(string[i:semi]): out += d[esc] else: print "XML Error: No such escape: `%s'" % esc i += len(esc) return out class FormWriter(object): def write(self, formxml, outfile): doc = libxml2.parseMemory(formxml, len(formxml)) doc.saveFile(outfile) doc.freeDoc() class HTMLFormWriter(FormWriter): def __init__(self, type, xsl_dir): self.xslpath = os.path.join(xsl_dir, "%s.xsl" % type) if not os.path.exists(self.xslpath): self.xslpath = os.path.join(xsl_dir, "default.xsl") def writeDoc(self, doc, outfile): print "Writing to %s" % outfile styledoc = libxml2.parseFile(self.xslpath) style = libxslt.parseStylesheetDoc(styledoc) result = style.applyStylesheet(doc, None) style.saveResultToFilename(outfile, result, 0) # FIXME!! #style.freeStylesheet() #styledoc.freeDoc() #doc.freeDoc() #result.freeDoc() def writeString(self, doc): styledoc = libxml2.parseFile(self.xslpath) style = libxslt.parseStylesheetDoc(styledoc) result = style.applyStylesheet(doc, None) return style.saveResultToString(result) class FieldWidget(object): def __init__(self, node): self.node = node self.caption = "Untitled Field" self.id = "unknown" self.type = (self.__class__.__name__.replace("Widget", "")).lower() self.widget = None self.vertical = False self.nolabel = False def set_caption(self, caption): self.caption = caption def set_id(self, id): self.id = id def make_container(self): return self.widget def get_widget(self): return self.make_container() def get_value(self): pass def set_value(self, value): pass def update_node(self): child = self.node.children while child: if child.type == "text": child.unlinkNode() child = child.next value = xml_escape(self.get_value()) if value: self.node.addContent(value) def set_editable(self, editable): if self.widget: self.widget.set_sensitive(editable) self.widget.set_style(STYLE_BRIGHT_INSENSITIVE) class TextWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) if node.children: text = xml_unescape(node.getContent().strip()) else: text = "" self.widget = gtk.Entry() self.widget.set_text(text) self.widget.show() def get_value(self): return self.widget.get_text() def set_value(self, value): self.widget.set_text(value) class ToggleWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) if node.children: try: status = eval(node.getContent().title()) except: print "Status of `%s' is invalid" % node.getContent() status = False else: status = False self.widget = gtk.CheckButton("Yes") self.widget.set_active(status) self.widget.show() def get_value(self): return str(self.widget.get_active()) class MultilineWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) self.vertical = True if node.children: text = xml_unescape(node.children.getContent().strip()) else: text = "" self.buffer = gtk.TextBuffer() self.buffer.set_text(text) self.widget = gtk.TextView(self.buffer) self.widget.show() self.widget.set_size_request(175, 200) self.widget.set_wrap_mode(gtk.WRAP_WORD) import mainapp config = mainapp.get_mainapp().config if config.getboolean("prefs", "check_spelling"): spell.prepare_TextBuffer(self.buffer) def make_container(self): sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add(self.widget) return sw vbox = gtk.VBox(False, 2) label = gtk.Label(self.caption) vbox.pack_start(label, 0,0,0) vbox.pack_start(sw, 0,0,0) label.show() vbox.show() sw.show() return vbox def get_value(self): return self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter()) def set_value(self, value): self.buffer.set_text(value) class DateWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) try: text = node.children.getContent().strip() (d, m, y) = text.split("-", 3) except: y = time.strftime("%Y") m = time.strftime("%b") d = time.strftime("%d") months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] days = [str("%02i" % x) for x in range(1,32)] years = [str(x) for x in range(int(y)-2, int(y)+2)] self.monthbox = make_choice(months, False, m) self.daybox = make_choice(days, False, d) self.yearbox = make_choice(years, False, y) self.widget = gtk.HBox(False, 2) self.widget.pack_start(self.monthbox, 0,0,0) self.widget.pack_start(self.daybox, 0,0,0) self.widget.pack_start(self.yearbox, 0,0,0) self.monthbox.show() self.daybox.show() self.yearbox.show() self.widget.show() def get_value(self): return "%s-%s-%s" % (self.daybox.get_active_text(), self.monthbox.get_active_text(), self.yearbox.get_active_text()) class TimeWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) try: text = node.children.getContent().strip() (h, m, s) = (int(x) for x in text.split(":", 3)) except: #FIXME #config = mainapp.get_mainapp().config if False and config.getboolean("prefs", "useutc"): t = time.gmtime() else: t = time.localtime() h = int(time.strftime("%H", t)) m = int(time.strftime("%M", t)) s = int(time.strftime("%S", t)) self.hour_a = gtk.Adjustment(h, 0, 23, 1) self.min_a = gtk.Adjustment(m, 0, 59, 1, 10) self.sec_a = gtk.Adjustment(s, 0, 59, 1, 10) self.hour = gtk.SpinButton(self.hour_a) self.min = gtk.SpinButton(self.min_a) self.sec = gtk.SpinButton(self.sec_a) self.widget = gtk.HBox(False, 2) self.widget.pack_start(self.hour, 0,0,0) self.widget.pack_start(self.min, 0,0,0) self.widget.pack_start(self.sec, 0,0,0) self.hour.show() self.min.show() self.sec.show() self.widget.show() def get_value(self): return "%.0f:%02.0f:%02.0f" % (self.hour_a.get_value(), self.min_a.get_value(), self.sec_a.get_value()) class NumericWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) try: min = float(node.prop("min")) except: min = 0 try: max = float(node.prop("max")) except: max = 1000000.0 try: initial = float(node.children.getContent()) except: initial = 0 self.adj = gtk.Adjustment(initial, min, max, 1, 10) self.widget = gtk.SpinButton(self.adj) self.widget.show() def get_value(self): return "%.0f" % self.adj.get_value() def set_value(self, value): self.adj.set_value(float(value)) class ChoiceWidget(FieldWidget): def parse_choice(self, node): if node.name != "choice": return try: content = xml_unescape(node.children.getContent().strip()) self.choices.append(content) if node.prop("set"): self.default = content except: pass def __init__(self, node): FieldWidget.__init__(self, node) self.choices = [] self.default = None child = node.children while child: if child.type == "element": self.parse_choice(child) child = child.next self.widget = make_choice(self.choices, False, self.default) self.widget.show() def get_value(self): return self.widget.get_active_text() def update_node(self): value = self.get_value() if not value: return child = self.node.children while child: if child.getContent() == value: if not child.hasProp("set"): child.newProp("set", "y") else: child.unsetProp("set") child = child.next class MultiselectWidget(FieldWidget): def parse_choice(self, node): if node.name != "choice": return try: content = xml_unescape(node.children.getContent().strip()) self.store.append(row=(node.prop("set") == "y", content)) self.choices.append((node.prop("set") == "y", content)) except Exception, e: print "Error: %s" % e pass def toggle(self, rend, path): self.store[path][0] = not self.store[path][0] def make_selector(self): self.store = gtk.ListStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING) self.view = gtk.TreeView(self.store) rend = gtk.CellRendererToggle() rend.connect("toggled", self.toggle) col = gtk.TreeViewColumn("", rend, active=0) self.view.append_column(col) rend = gtk.CellRendererText() col = gtk.TreeViewColumn("", rend, text=1) self.view.append_column(col) self.view.show() self.view.set_headers_visible(False) return self.view def __init__(self, node): FieldWidget.__init__(self, node) self.choices = [] self.widget = self.make_selector() self.widget.show() child = node.children while child: if child.type == "element": self.parse_choice(child) child = child.next def make_container(self): vbox = gtk.VBox(False, 2) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add(self.widget) if self.caption: label = gtk.Label(self.caption) vbox.pack_start(label, 0,0,0) label.show() vbox.pack_start(sw, 0,0,0) vbox.show() sw.show() return vbox def get_value(self): return "" def update_node(self): vals = {} iter = self.store.get_iter_first() while iter: setval, name = self.store.get(iter, 0, 1) vals[name] = setval iter = self.store.iter_next(iter) child = self.node.children while child: choice = child.getContent().strip() if choice not in vals.keys(): vals[choice] = False if not child.hasProp("set"): child.newProp("set", vals[choice] and "y" or "n") else: child.setProp("set", vals[choice] and "y" or "n") child = child.next class LabelWidget(FieldWidget): def __init__(self, node): FieldWidget.__init__(self, node) self.nolabel = True def update_node(self): pass def make_container(self): widget = gtk.Label() widget.set_markup("%s" % self.caption) color = gtk.gdk.color_parse("blue") #widget.modify_fg(gtk.STATE_NORMAL, color) widget.show() return widget class FormField(object): widget_types = { "text" : TextWidget, "multiline" : MultilineWidget, "toggle" : ToggleWidget, "date" : DateWidget, "time" : TimeWidget, "numeric" : NumericWidget, "choice" : ChoiceWidget, "multiselect" : MultiselectWidget, "label" : LabelWidget, } def set_editable(self, editable): self.entry.set_editable(editable) def get_caption_string(self, node): return node.getContent().strip() def build_entry(self, node, caption): type = node.prop("type") wtype = self.widget_types[type] field = wtype(node) field.set_caption(caption) field.set_id(self.id) return field def build_gui(self, node): self.caption = None self.entry = None child = node.children while child: if child.name == "caption": cap_node = child elif child.name == "entry": ent_node = child child = child.next self.caption = self.get_caption_string(cap_node) self.entry = self.build_entry(ent_node, self.caption) self.widget = self.entry.get_widget() self.widget.show() def __init__(self, field): self.node = field self.id = field.prop("id") self.build_gui(field) def get_widget(self): return self.widget def update_node(self): self.entry.update_node() class FormFile(object): def __init__(self, filename): self._filename = filename f = file(self._filename) data = f.read() f.close() if not data: raise Exception("Form file %s is empty!" % filename) self.fields = [] self.doc = libxml2.parseMemory(data, len(data)) self.process_form(self.doc) def configure(self, config): self.xsl_dir = config.form_source_dir() def __del__(self): self.doc.freeDoc() def save_to(self, filename): f = file(filename, "w") print >>f, self.doc.serialize() f.close() def export_to_string(self): w = HTMLFormWriter(self.id, self.xsl_dir) return w.writeString(self.doc) def get_xml(self): return self.doc.serialize() def process_form(self, doc): ctx = doc.xpathNewContext() forms = ctx.xpathEval("//form") if len(forms) != 1: raise Exception("%i forms in document" % len(forms)) form = forms[0] self.id = form.prop("id") titles = ctx.xpathEval("//form/title") if len(titles) != 1: raise Exception("%i titles in document" % len(titles)) title = titles[0] self.title_text = title.children.getContent().strip() logos = ctx.xpathEval("//form/logo") if len(logos) > 1: raise Exception("%i logos in document" % len(logos)) elif len(logos) == 1: logo = logos[0] self.logo_path = logo.children.getContent().strip() else: self.logo_path = None ctx.xpathFreeContext() def __set_content(self, node, content): child = node.children while child: if child.type == "text": child.unlinkNode() child = child.next node.addContent(content) def __get_xpath(self, path): ctx = self.doc.xpathNewContext() result = ctx.xpathEval(path) ctx.xpathFreeContext() return result def get_path(self): pathels = [] for element in self.__get_xpath("//form/path/e"): pathels.append(element.getContent().strip()) return pathels def __get_path(self): els = self.__get_xpath("//form/path") if not els: ctx = self.doc.xpathNewContext() form, = ctx.xpathEval("//form") ctx.xpathFreeContext() return form.newChild(None, "path", None) else: return els[0] def __add_path_element(self, name, element, append=False): path = self.__get_path() if append: path.newChild(None, name, element) return els = self.__get_xpath("//form/path/%s" % name) if not els: path.newChild(None, name, element) return self.__set_content(els[0], element) def add_path_element(self, element): self.__add_path_element("e", element, True) def set_path_src(self, src): self.__add_path_element("src", src) def set_path_dst(self, dst): self.__add_path_element("dst", dst) def set_path_mid(self, mid): self.__add_path_element("mid", mid) def __get_path_element(self, name): els = self.__get_xpath("//form/path/%s" % name) if els: return els[0].getContent().strip() else: return "" def get_path_src(self): return self.__get_path_element("src") def get_path_dst(self): return self.__get_path_element("dst") def get_path_mid(self): return self.__get_path_element("mid") def get_field_value(self, id): els = self.__get_xpath("//form/field[@id='%s']/entry" % id) if len(els) == 1: return xml_unescape(els[0].getContent().strip()) elif len(els) > 1: raise Exception("More than one id=%s node!" % id) else: return None def set_field_value(self, id, value): els = self.__get_xpath("//form/field[@id='%s']/entry" % id) print "Setting %s to %s (%i)" % (id, value, len(els)) if len(els) == 1: if els[0].prop("type") == "multiline": self.__set_content(els[0], value) else: self.__set_content(els[0], value.strip()) def _try_get_fields(self, *names): for field in names: try: val = self.get_field_value(field) if val is not None: return val except Exception, e: pass return "Unknown" def get_subject_string(self): subj = self._try_get_fields("_auto_subject", "subject") if subj != "Unknown": return subj.replace("\r", "").replace("\n", "") return "%s#%s" % (self.get_path_src(), self._try_get_fields("_auto_number")) def get_recipient_string(self): dst = self.get_path_dst() if dst: return dst else: return self._try_get_fields("_auto_recip", "recip", "recipient") def get_sender_string(self): src = self.get_path_src() if src: return src else: return self._try_get_fields("_auto_sender", "sender") def get_attachments(self): atts = [] els = self.__get_xpath("//form/att") for el in els: name = el.prop("name") data = el.getContent() atts.append((name, len(data))) return atts def get_attachment(self, name): els = self.__get_xpath("//form/att[@name='%s']" % name) if len(els) == 1: data = els[0].getContent() data = base64.b64decode(data) return zlib.decompress(data) else: raise Exception("Internal Error: %i attachments named `%s'" % \ (len(els), name)) def add_attachment(self, name, data): try: att = self.get_attachment(name) except Exception, e: att = None if att is not None: raise Exception("Already have an attachment named `%s'" % name) els = self.__get_xpath("//form") if len(els) == 1: attnode = els[0].newChild(None, "att", None) attnode.setProp("name", name) data = zlib.compress(data, 9) data = base64.b64encode(data) self.__set_content(attnode, data) def del_attachment(self, name): els = self.__get_xpath("//form/att[@name='%s']" % name) if len(els) == 1: els[0].unlinkNode() class FormDialog(FormFile, gtk.Dialog): def save_to(self, *args): for f in self.fields: f.update_node() FormFile.save_to(self, *args) def process_fields(self, doc): ctx = doc.xpathNewContext() fields = ctx.xpathEval("//form/field") ctx.xpathFreeContext() for f in fields: try: self.fields.append(FormField(f)) except Exception, e: raise print e def export(self, outfile): for f in self.fields: f.update_node() w = HTMLFormWriter(self.id, self.xsl_dir) w.writeDoc(self.doc, outfile) def run_auto(self, save_file=None): if not save_file: save_file = self._filename r = self.run() if r != gtk.RESPONSE_CANCEL: self.save_to(save_file) return r def but_save(self, widget, data=None): p = platform.get_platform() f = p.gui_save_file(default_name="%s.html" % self.id) if not f: return try: self.export(f) except Exception, e: ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) ed.text = "Unable to open file" ed.format_secondary_text("Unable to open %s (%s)" % (f, e)) ed.run() ed.destroy() def but_printable(self, widget, data=None): f = tempfile.NamedTemporaryFile(suffix=".html") name = f.name f.close() self.export(name) print "Exported to temporary file: %s" % name platform.get_platform().open_html_file(name) def calc_check(self, buffer, checkwidget): message = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter()) checkwidget.set_text("%i" % len(message.split())) def build_routing_widget(self): tab = gtk.Table(2, 2) lab = gtk.Label(_("Source Callsign")) lab.show() tab.attach(lab, 0, 1, 0, 1, 0, 0, 2, 5) lab = gtk.Label(_("Destination Callsign")) lab.show() tab.attach(lab, 0, 1, 1, 2, 0, 0, 2, 5) srcbox = gtk.Entry() srcbox.set_text(self.get_path_src()) srcbox.set_editable(False) srcbox.show() self._srcbox = srcbox tab.attach(srcbox, 1, 2, 0, 1, gtk.EXPAND|gtk.FILL, 0) dstbox = gtk.Entry() dstbox.set_text(self.get_path_dst()) dstbox.show() self._dstbox = dstbox tab.attach(dstbox, 1, 2, 1, 2, gtk.EXPAND|gtk.FILL, 0) exp = gtk.Expander() exp.set_label(_("Routing Information")) exp.add(tab) tab.show() exp.set_expanded(True) exp.show() return exp def build_path_widget(self): pathels = self.get_path() pathbox = gtk.Entry() pathbox.set_text(";".join(pathels)) pathbox.set_property("editable", False) pathbox.show() expander = gtk.Expander("Path") expander.add(pathbox) expander.show() return expander def build_att_widget(self): hbox = gtk.HBox(False, 2) cols = [(gobject.TYPE_STRING, "KEY"), (gobject.TYPE_STRING, _("Name")), (gobject.TYPE_INT, _("Size (bytes)"))] self.attbox = KeyedListWidget(cols) self.attbox.set_resizable(0, True) self.attbox.set_expander(0) self.attbox.show() sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add_with_viewport(self.attbox) sw.show() hbox.pack_start(sw, 1, 1, 1) def item_set(box, key): natt = len(self.attbox.get_keys()) print "Item %s set: %i\n" % (key, natt) if natt: msg = _("Attachments") + " (%i)" % natt attexp.set_label("%s" % msg) else: attexp.set_label(_("Attachments")) self.attbox.connect("item-set", item_set) bbox = gtk.VBox(False, 2) bbox.show() hbox.pack_start(bbox, 0, 0, 0) @run_or_error def but_add(but): fn = platform.get_platform().gui_open_file() if fn: name = os.path.basename(fn) f = file(fn, "rb") data = f.read() f.close() self.add_attachment(name, data) self.attbox.set_item(name, name, len(data)) add = gtk.Button(_("Add"), gtk.STOCK_ADD) add.connect("clicked", but_add) add.show() bbox.pack_start(add, 0, 0, 0) @run_or_error def but_rem(but): name = self.attbox.get_selected() self.del_attachment(name) self.attbox.del_item(name) item_set(None, name) rem = gtk.Button(_("Remove"), gtk.STOCK_REMOVE) rem.connect("clicked", but_rem) rem.show() bbox.pack_start(rem, 0, 0, 0) @run_or_error def but_sav(but): name = self.attbox.get_selected() if not name: return fn = platform.get_platform().gui_save_file(default_name=name) if fn: f = file(fn, "wb") data = self.get_attachment(name) if not data: raise Exception("Unable to extract attachment") f.write(data) f.close() sav = gtk.Button(_("Save"), gtk.STOCK_SAVE) sav.connect("clicked", but_sav) sav.show() bbox.pack_start(sav, 0, 0, 0) attexp = gtk.Expander(_("Attachments")) attexp.set_use_markup(True) hbox.show() attexp.add(hbox) attexp.show() atts = self.get_attachments() for name, size in atts: self.attbox.set_item(name, name, size) return attexp def build_toolbar(self, editable): tb = gtk.Toolbar() def close(but, *args): print "Closing" if editable: d = ask_for_confirmation("Close without saving?", self) if not d: return True w, h = self.get_size() self._config.set("state", "form_%s_x" % self.id, str(w)) self._config.set("state", "form_%s_y" % self.id, str(h)) self.response(gtk.RESPONSE_CLOSE) return False def save(but): self.response(RESPONSE_SAVE) def send(but): self.response(RESPONSE_SEND) def svia(but): self.response(RESPONSE_SEND_VIA) def reply(but): self.response(RESPONSE_REPLY) def delete(but): self.response(RESPONSE_DELETE) # We have to get in the way of the RESPONSE_DELETE_EVENT signal # to be able to catch the save # http://faq.pygtk.org/index.py?req=show&file=faq10.013.htp def reject_delete_response(dialog, response, *args): if response == gtk.RESPONSE_DELETE_EVENT: dialog.emit_stop_by_name("response") send_tip = "Place this message in the Outbox for sending" svia_tip = "Place this message in the Outbox and send it directly " +\ "to a specific station for relay" save_tip = "Save changes to this message" prnt_tip = "View the printable version of this form" rply_tip = "Compose a reply to this message" dele_tip = "Delete this message" if editable: buttons = [ (gtk.STOCK_SAVE, "", save_tip, save), ("msg-send.png", _("Send"), send_tip, send), ("msg-send-via.png", _("Send via"), svia_tip, svia), (gtk.STOCK_PRINT, "", prnt_tip, self.but_printable), ] else: buttons = [ ("msg-reply.png", _("Reply"), rply_tip, reply), #("msg-send.png", _("Forward"), send_tip, send), ("msg-send-via.png", _("Forward via"), svia_tip, svia), (gtk.STOCK_PRINT, "", prnt_tip, self.but_printable), (gtk.STOCK_DELETE, "", dele_tip, delete), ] #self.connect("destroy", close) self.connect("delete-event", close) self.connect("response", reject_delete_response) i = 0 for img, lab, tip, func in buttons: if not lab: ti = gtk.ToolButton(img) else: icon = gtk.Image() icon.set_from_pixbuf(self._config.ship_img(img)) icon.show() ti = gtk.ToolButton(icon, lab) ti.show() try: ti.set_tooltip_text(tip) except AttributeError: pass ti.connect("clicked", func) tb.insert(ti, i) i += 1 tb.show() return tb def build_gui(self, editable=True): self.vbox.pack_start(self.build_toolbar(editable), 0, 0, 0) tlabel = gtk.Label() tlabel.set_markup("%s" % self.title_text) tlabel.show() if self.logo_path: image = gtk.Image() try: base = self._config.get("settings", "form_logo_dir") print "Logo path: %s" % os.path.join(base, self.logo_path) image.set_from_file(os.path.join(base, self.logo_path)) self.vbox.pack_start(image, 0,0,0) image.show() except Exception, e: print "Unable to load or display logo %s: %s" % (self.logo_path, e) self.vbox.pack_start(tlabel, 0,0,0) self.vbox.pack_start(self.build_routing_widget(), 0, 0, 0) #field_box = gtk.VBox(False, 2) field_box = gtk.Table(len(self.fields), 2) row = 0 sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.add_with_viewport(field_box) field_box.show() sw.show() self.vbox.pack_start(sw, 1,1,1) msg_field = None chk_field = None for f in self.fields: if f.id == "_auto_check": chk_field = f elif f.id == "_auto_message": msg_field = f elif f.id == "_auto_senderX": # FIXME if not f.entry.widget.get_text(): call = self._config.get("user", "callsign") f.entry.widget.set_text(call) f.entry.widget.set_property("editable", False) elif f.id == "_auto_position": if not f.entry.widget.get_text(): import mainapp # Dirty hack pos = mainapp.get_mainapp().get_position() f.entry.widget.set_text(pos.coordinates()) l = gtk.Label(f.caption) l.show() w = f.get_widget() if f.entry.vertical: field_box.attach(l, 0, 2, row, row+1, gtk.SHRINK, gtk.SHRINK) row += 1 field_box.attach(w, 0, 2, row, row+1) elif f.entry.nolabel: field_box.attach(w, 0, 2, row, row+1, gtk.SHRINK, gtk.SHRINK) else: field_box.attach(l, 0, 1, row, row+1, gtk.SHRINK, gtk.SHRINK, 5) field_box.attach(w, 1, 2, row, row+1, yoptions=0) row += 1 self.vbox.pack_start(self.build_att_widget(), 0, 0, 0) self.vbox.pack_start(self.build_path_widget(), 0, 0, 0) if msg_field and chk_field: mw = msg_field.entry.buffer cw = chk_field.entry.widget mw.connect("changed", self.calc_check, cw) self.set_editable(editable) def __init__(self, title, filename, buttons=None, parent=None): self._buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK) if buttons: self._buttons += buttons gtk.Dialog.__init__(self, buttons=(), parent=parent) FormFile.__init__(self, filename) import mainapp self._config = mainapp.get_mainapp().config self.process_fields(self.doc) self.set_title(self.title_text) try: x = self._config.getint("state", "form_%s_x" % self.id) y = self._config.getint("state", "form_%s_y" % self.id) except Exception, e: print "Unable to get form_%s_*: %s" % (self.id, e) x = 300 y = 500 self.set_default_size(x, y) print "Form ID: %s" % self.id def update_dst(self): dst = self._dstbox.get_text() if "@" not in dst: dst = dst.upper() self.set_path_dst(dst) def run(self): self.vbox.set_spacing(5) self.build_gui() self.set_size_request(380, 450) r = gtk.Dialog.run(self) self.update_dst() return r def set_editable(self, editable): for field in self.fields: field.set_editable(editable) if __name__ == "__main__": f = file(sys.argv[1]) xml = f.read() form = Form("Form", xml) form.run() form.destroy() try: gtk.main() except: pass print form.get_text() d-rats-0.3.3/d_rats/geocode_ui.py000066400000000000000000000152341160617671700166600ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject import miscwidgets from geopy import geocoders YID = "eHRO5K_V34FXWnljF5BJYvTc.lXh.kQ0MaJpnq3BhgaX.IJrvtd6cvGgtWEPNAb7" try: from gtk import Assistant as baseclass except ImportError: print "No gtk.Assistant support" class baseclass(gtk.MessageDialog): __gsignals__ = { "prepare" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "cancel" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "apply" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } def __init__(self): gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK) self.set_property("text", "Unsupported") self.format_secondary_text("The version of GTK you're using " "is too old and does not support " "the 'Assistant' class. This is " "required for geocoding support.") def close(d, r): self.destroy() # make the dialog go away gtk.main_quit() # The run method calls gtk.main() self.connect("response", close) def append_page(self, *a, **k): pass def set_page_type(self, *a, **k): pass def set_page_title(self, *a, **k): pass class AddressAssistant(baseclass): def make_address_entry_page(self): def complete_cb(label, page): self.set_page_complete(page, len(label.get_text()) > 1) vbox = gtk.VBox(False, 0) lab = gtk.Label(_("Enter an address, postal code, or intersection") +\ ":") lab.show() vbox.pack_start(lab, 1, 1, 1) ent = gtk.Entry() ent.connect("changed", complete_cb, vbox) ent.show() vbox.pack_start(ent, 0, 0, 0) self.vals["_address"] = ent vbox.show() return vbox def make_address_selection(self): cols = [ (gobject.TYPE_STRING, _("Address")), (gobject.TYPE_FLOAT, _("Latitude")), (gobject.TYPE_FLOAT, _("Longitude")) ] listbox = miscwidgets.ListWidget(cols) self.vals["AddressList"] = listbox listbox.show() return listbox def make_address_confirm_page(self): vbox = gtk.VBox(False, 0) def make_kv(key, value): hbox = gtk.HBox(False, 2) lab = gtk.Label(key) lab.set_size_request(100, -1) lab.show() hbox.pack_start(lab, 0, 0, 0) lab = gtk.Label(value) lab.show() hbox.pack_start(lab, 0, 0, 0) self.vals[key] = lab hbox.show() return hbox vbox.pack_start(make_kv(_("Address"), ""), 0, 0, 0) vbox.pack_start(make_kv(_("Latitude"), ""), 0, 0, 0) vbox.pack_start(make_kv(_("Longitude"), ""), 0, 0, 0) vbox.show() return vbox def prepare_sel(self, assistant, page): address = self.vals["_address"].get_text() if not address: return try: g = geocoders.Yahoo(YID) places = g.geocode(address, exactly_one=False) self.set_page_complete(page, True) except Exception, e: print "Did not find `%s': %s" % (address, e) places = [] lat = lon = 0 self.set_page_complete(page, False) i = 0 self.vals["AddressList"].set_values([]) for place, (lat, lon) in places: i += 1 self.vals["AddressList"].add_item(place, lat, lon) if i == -1: page.hide() self.set_current_page(self.get_current_page() + 1) def prepare_conf(self, assistant, page): self.place, self.lat, self.lon = self.vals["AddressList"].get_selected(True) self.vals[_("Address")].set_text(self.place) self.vals[_("Latitude")].set_text("%.5f" % self.lat) self.vals[_("Longitude")].set_text("%.5f" % self.lon) self.set_page_complete(page, True) def prepare_page(self, assistant, page): if page == self.sel_page: print "Sel" return self.prepare_sel(assistant, page) elif page == self.conf_page: print "Conf" return self.prepare_conf(assistant, page) elif page == self.entry_page: print "Ent" self.sel_page.show() else: print "I dunno" def exit(self, _, response): self.response = response gtk.main_quit() def run(self): self.show() self.set_modal(True) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) gtk.main() self.hide() return self.response def __init__(self): baseclass.__init__(self) self.response = None self.vals = {} self.place = self.lat = self.lon = None self.entry_page = self.make_address_entry_page() self.append_page(self.entry_page) self.set_page_title(self.entry_page, _("Locate an address")) self.set_page_type(self.entry_page, gtk.ASSISTANT_PAGE_CONTENT) self.sel_page = self.make_address_selection() self.append_page(self.sel_page) self.set_page_title(self.sel_page, _("Locations found")) self.set_page_type(self.sel_page, gtk.ASSISTANT_PAGE_CONTENT) self.conf_page = self.make_address_confirm_page() self.append_page(self.conf_page) self.set_page_title(self.conf_page, _("Confirm address")) self.set_page_type(self.conf_page, gtk.ASSISTANT_PAGE_CONFIRM) self.connect("prepare", self.prepare_page) self.set_size_request(500, 300) self.connect("cancel", self.exit, gtk.RESPONSE_CANCEL) self.connect("apply", self.exit, gtk.RESPONSE_OK) if __name__ == "__main__": a = AddressAssistant() a.show() gtk.main() d-rats-0.3.3/d_rats/geopy/000077500000000000000000000000001160617671700153225ustar00rootroot00000000000000d-rats-0.3.3/d_rats/geopy/__init__.py000066400000000000000000000000211160617671700174240ustar00rootroot00000000000000import geocoders d-rats-0.3.3/d_rats/geopy/distance.py000066400000000000000000000366611160617671700175020ustar00rootroot00000000000000from math import * import util # Average great-circle radius in kilometers, from Wikipedia. # Using a sphere with this radius results in an error of up to about 0.5%. EARTH_RADIUS = 6372.795 # From http://www.movable-type.co.uk/scripts/LatLongVincenty.html: # The most accurate and widely used globally-applicable model for the earth # ellipsoid is WGS-84, used in this script. Other ellipsoids offering a # better fit to the local geoid include Airy (1830) in the UK, International # 1924 in much of Europe, Clarke (1880) in Africa, and GRS-67 in South # America. America (NAD83) and Australia (GDA) use GRS-80, functionally # equivalent to the WGS-84 ellipsoid. # # model major (km) minor (km) flattening ELLIPSOIDS = {'WGS-84': (6378.137, 6356.7523142, 1 / 298.257223563), 'GRS-80': (6378.137, 6356.7523141, 1 / 298.257222101), 'Airy (1830)': (6377.563396, 6356.256909, 1 / 299.3249646), 'Intl 1924': (6378.388, 6356.911946, 1 / 297.0), 'Clarke (1880)': (6378.249145, 6356.51486955, 1 / 293.465), 'GRS-67': (6378.1600, 6356.774719, 1 / 298.25), } def arc_degrees(arcminutes=0, arcseconds=0): """Calculate the decimal equivalent of the sum of ``arcminutes`` and ``arcseconds`` in degrees.""" if arcminutes is None: arcminutes = 0 if arcseconds is None: arcseconds = 0 arcmin = float(arcminutes) arcsec = float(arcseconds) return arcmin * 1 / 60. + arcsec * 1 / 3600. def kilometers(miles=0, feet=0, nautical=0): d = 0 if feet: miles += feet / ft(1.0) if nautical: d += nautical / nm(1.0) d += miles * 1.609344 return d def miles(kilometers=0, feet=0, nautical=0): d = 0 if nautical: kilometers += nautical / nm(1.0) if feet: d += feet / ft(1.0) d += kilometers * 0.621371192 return d def feet(miles=0, kilometers=0, nautical=0): d = 0 if nautical: kilometers += nautical / nm(1.0) if kilometers: miles += mi(kilometers) d += miles * 5280 return d def nautical(kilometers=0, miles=0, feet=0): d = 0 if feet: miles += feet / ft(1.0) if miles: kilometers += km(miles) d += kilometers / 1.852 return d km = kilometers mi = miles ft = feet nm = nautical class Distance(object): def __init__(self, kilometers=0, miles=0, feet=0, nautical=0): """Initialize a Distance whose length is the sum of all the units measured in the constructor (kilometers, miles, feet, nautical miles). """ kilometers += km(miles=miles, feet=feet, nautical=nautical) self._kilometers = kilometers @property def kilometers(self): return self._kilometers @property def miles(self): return miles(self.kilometers) @property def feet(self): return feet(self.miles) @property def nautical(self): return nautical(self.kilometers) # Sadly, just aliasing the above properties with their abbreviations does # not work when they are subclassed. The easiest way I could find to # make this work without using a metaclass was to write more full-fledged # definitions... @property def mi(self): return self.miles @property def km(self): return self.kilometers @property def ft(self): return self.feet @property def nm(self): return self.nautical def __add__(self, other): """Return a new Distance of length ``self`` + ``other``.""" if isinstance(other, Distance): return Distance(self.kilometers + other.kilometers) else: raise TypeError("Distance must be added with Distance.") def __sub__(self, other): """Return a new Distance of length ``self`` - ``other``.""" if isinstance(other, Distance): return Distance(self.kilometers - other.kilometers) else: raise TypeError("Distance must be subtracted from Distance.") def __mul__(self, other): """Return a new Distance ``other`` times the length of ``self``, ``other`` is a number (int, float, long, or Decimal). """ if isinstance(other, (int, float, long, Decimal)): other = float(other) return Distance(self.kilometers * other) else: raise TypeError("Distance must be multiplied by number.") def __div__(self, other): """If ``other`` is a number (int, float, long, or Decimal), return a new Distance of length ``self`` / ``other``. If ``other`` is a Distance, return the fraction given by ``self`` / ``other``. """ if isinstance(other, Distance): return float(self.kilometers) / other.kilometers elif isinstance(other, (int, float, long, Decimal)): other = float(other) return Distance(self.kilometers / other) else: raise TypeError("Distance must be divided by Distance or number.") def __nonzero__(self): """Return whether or not this Distance is 0 units in length.""" return bool(self.kilometers) class GeodesicDistance(Distance): def __init__(self, a, b): """Initialize a Distance whose length is the distance between the two geodesic points ``a`` and ``b``, using the ``calculate`` method to determine this distance. """ if isinstance(a, basestring): a = util.parse_geo(a) if isinstance(b, basestring): b = util.parse_geo(b) self.a = a self.b = b if a and b: self.calculate() def calculate(self): """Calculate and set the distance between ``self.a`` and ``self.b``, which should be two geodesic points. Since there are multiple formulas to calculate this, implementation is left up to the subclass. """ raise NotImplementedError @property def kilometers(self): raise NotImplementedError class GreatCircleDistance(GeodesicDistance): """Use spherical geometry to calculate the surface distance between two geodesic points. This formula can be written many different ways, including just the use of the spherical law of cosines or the haversine formula. The class member ``RADIUS`` indicates which radius of the earth to use, in kilometers. The default is to use the module constant ``EARTH_RADIUS``, which uses the average great-circle radius. """ RADIUS = EARTH_RADIUS def calculate(self): lat1, lng1 = map(radians, self.a) lat2, lng2 = map(radians, self.b) sin_lat1, cos_lat1 = sin(lat1), cos(lat1) sin_lat2, cos_lat2 = sin(lat2), cos(lat2) delta_lng = lng2 - lng1 cos_delta_lng, sin_delta_lng = cos(delta_lng), sin(delta_lng) central_angle = acos(sin_lat1 * sin_lat2 + cos_lat1 * cos_lat2 * cos_delta_lng) # From http://en.wikipedia.org/wiki/Great_circle_distance: # Historically, the use of this formula was simplified by the # availability of tables for the haversine function. Although this # formula is accurate for most distances, it too suffers from # rounding errors for the special (and somewhat unusual) case of # antipodal points (on opposite ends of the sphere). A more # complicated formula that is accurate for all distances is: (below) d = atan2(sqrt((cos_lat2 * sin_delta_lng) ** 2 + (cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_delta_lng) ** 2), sin_lat1 * sin_lat2 + cos_lat1 * cos_lat2 * cos_delta_lng) self.radians = d @property def kilometers(self): return self.RADIUS * self.radians class VincentyDistance(GeodesicDistance): """Calculate the geodesic distance between two points using the formula devised by Thaddeus Vincenty, with an accurate ellipsoidal model of the earth. The class attribute ``ELLIPSOID`` indicates which ellipsoidal model of the earth to use. If it is a string, it is looked up in the ELLIPSOIDS dictionary to obtain the major and minor semiaxes and the flattening. Otherwise, it should be a tuple with those values. The most globally accurate model is WGS-84. See the comments above the ELLIPSOIDS dictionary for more information. """ ELLIPSOID = ELLIPSOIDS['WGS-84'] def calculate(self): lat1, lng1 = map(radians, self.a) lat2, lng2 = map(radians, self.b) if isinstance(self.ELLIPSOID, basestring): major, minor, f = ELLIPSOIDS[self.ELLIPSOID] else: major, minor, f = self.ELLIPSOID delta_lng = lng2 - lng1 reduced_lat1 = atan((1 - f) * tan(lat1)) reduced_lat2 = atan((1 - f) * tan(lat2)) sin_reduced1, cos_reduced1 = sin(reduced_lat1), cos(reduced_lat1) sin_reduced2, cos_reduced2 = sin(reduced_lat2), cos(reduced_lat2) lambda_lng = delta_lng lambda_prime = 2 * pi iter_limit = 20 while abs(lambda_lng - lambda_prime) > 10e-12 and iter_limit > 0: sin_lambda_lng, cos_lambda_lng = sin(lambda_lng), cos(lambda_lng) sin_sigma = sqrt((cos_reduced2 * sin_lambda_lng) ** 2 + (cos_reduced1 * sin_reduced2 - sin_reduced1 * cos_reduced2 * cos_lambda_lng) ** 2) if sin_sigma == 0: # Coincident points self._kilometers = self.initial_bearing = self.final_bearing = 0 return cos_sigma = (sin_reduced1 * sin_reduced2 + cos_reduced1 * cos_reduced2 * cos_lambda_lng) sigma = atan2(sin_sigma, cos_sigma) sin_alpha = cos_reduced1 * cos_reduced2 * sin_lambda_lng / sin_sigma cos_sq_alpha = 1 - sin_alpha ** 2 if cos_sq_alpha != 0: cos2_sigma_m = cos_sigma - 2 * (sin_reduced1 * sin_reduced2 / cos_sq_alpha) else: cos2_sigma_m = 0.0 # Equatorial line C = f / 16. * cos_sq_alpha * (4 + f * (4 - 3 * cos_sq_alpha)) lambda_prime = lambda_lng lambda_lng = (delta_lng + (1 - C) * f * sin_alpha * (sigma + C * sin_sigma * (cos2_sigma_m + C * cos_sigma * (-1 + 2 * cos2_sigma_m ** 2)))) iter_limit -= 1 if iter_limit == 0: raise ValueError("Vincenty formula failed to converge!") u_sq = cos_sq_alpha * (major ** 2 - minor ** 2) / minor ** 2 A = 1 + u_sq / 16384. * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq))) B = u_sq / 1024. * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq))) delta_sigma = (B * sin_sigma * (cos2_sigma_m + B / 4. * (cos_sigma * (-1 + 2 * cos2_sigma_m ** 2) - B / 6. * cos2_sigma_m * (-3 + 4 * sin_sigma ** 2) * (-3 + 4 * cos2_sigma_m ** 2)))) s = minor * A * (sigma - delta_sigma) sin_lambda, cos_lambda = sin(lambda_lng), cos(lambda_lng) alpha_1 = atan2(cos_reduced2 * sin_lambda, cos_reduced1 * sin_reduced2 - sin_reduced1 * cos_reduced2 * cos_lambda) alpha_2 = atan2(cos_reduced1 * sin_lambda, cos_reduced1 * sin_reduced2 * cos_lambda - sin_reduced1 * cos_reduced2) self._kilometers = s self.initial_bearing = (360 + degrees(alpha_1)) % 360 self.final_bearing = (360 + degrees(alpha_2)) % 360 @property def kilometers(self): return self._kilometers @property def forward_azimuth(self): return self.initial_bearing # Set the default distance formula to the most generally accurate. distance = VincentyDistance def destination(start, bearing, distance, radius=EARTH_RADIUS): lat1, lng1 = map(radians, start) bearing = radians(bearing) if isinstance(distance, Distance): distance = distance.kilometers d_div_r = float(distance) / radius lat2 = asin(sin(lat1) * cos(d_div_r) + cos(lat1) * sin(d_div_r) * cos(bearing)) lng2 = lng1 + atan2(sin(bearing) * sin(d_div_r) * cos(lat1), cos(d_div_r) - sin(lat1) * sin(lat2)) return (degrees(lat2), degrees(lng2)) def vincenty_destination(start, bearing, distance, ellipsoid=ELLIPSOIDS['WGS-84']): lat1, lng1 = map(radians, start) bearing = radians(bearing) if isinstance(distance, Distance): distance = distance.kilometers if isinstance(ellipsoid, basestring): ellipsoid = ELLIPSOIDS[ellipsoid] major, minor, f = ellipsoid tan_reduced1 = (1 - f) * tan(lat1) cos_reduced1 = 1 / sqrt(1 + tan_reduced1 ** 2) sin_reduced1 = tan_reduced1 * cos_reduced1 sin_bearing, cos_bearing = sin(bearing), cos(bearing) sigma1 = atan2(tan_reduced1, cos_bearing) sin_alpha = cos_reduced1 * sin_bearing cos_sq_alpha = 1 - sin_alpha ** 2 u_sq = cos_sq_alpha * (major ** 2 - minor ** 2) / minor ** 2 A = 1 + u_sq / 16384. * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq))) B = u_sq / 1024. * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq))) sigma = distance / (minor * A) sigma_prime = 2 * pi while abs(sigma - sigma_prime) > 10e-12: cos2_sigma_m = cos(2 * sigma1 + sigma) sin_sigma, cos_sigma = sin(sigma), cos(sigma) delta_sigma = B * sin_sigma * (cos2_sigma_m + B / 4. * (cos_sigma * (-1 + 2 * cos2_sigma_m) - B / 6. * cos2_sigma_m * (-3 + 4 * sin_sigma ** 2) * (-3 + 4 * cos2_sigma_m ** 2))) sigma_prime = sigma sigma = distance / (minor * A) + delta_sigma sin_sigma, cos_sigma = sin(sigma), cos(sigma) lat2 = atan2(sin_reduced1 * cos_sigma + cos_reduced1 * sin_sigma * cos_bearing, (1 - f) * sqrt(sin_alpha ** 2 + (sin_reduced1 * sin_sigma - cos_reduced1 * cos_sigma * cos_bearing) ** 2)) lambda_lng = atan2(sin_sigma * sin_bearing, cos_reduced1 * cos_sigma - sin_reduced1 * sin_sigma * cos_bearing) C = f / 16. * cos_sq_alpha * (4 + f * (4 - 3 * cos_sq_alpha)) delta_lng = (lambda_lng - (1 - C) * f * sin_alpha * (sigma + C * sin_sigma * (cos2_sigma_m + C * cos_sigma * (-1 + 2 * cos2_sigma_m ** 2)))) final_bearing = atan2(sin_alpha, cos_reduced1 * cos_sigma * cos_bearing - sin_reduced1 * sin_sigma) lng2 = lng1 + delta_lng return (degrees(lat2), degrees(lng2)) d-rats-0.3.3/d_rats/geopy/geocoders.py000066400000000000000000000727131160617671700176600ustar00rootroot00000000000000import re import csv import sys import getpass import xmlrpclib import htmlentitydefs import xml.dom.minidom from itertools import groupby from urllib import quote_plus, urlencode from urllib2 import urlopen, HTTPError from xml.parsers.expat import ExpatError try: set except NameError: import sets.Set as set # Other submodules from geopy: import util # Now try some more exotic modules... try: from BeautifulSoup import BeautifulSoup except ImportError: pass try: import simplejson except ImportError: try: from django.utils import simplejson except ImportError: print "simplejson was not found. " \ "Geocoders relying on JSON parsing will not work." class Geocoder(object): """Base class for all geocoders.""" def geocode(self, string): raise NotImplementedError class WebGeocoder(Geocoder): """A Geocoder subclass with utility methods helpful for handling results given by web-based geocoders.""" @classmethod def _get_encoding(cls, page, contents=None): """Get the last encoding (charset) listed in the header of ``page``.""" plist = page.headers.getplist() if plist: key, value = plist[-1].split('=') if key.lower() == 'charset': return value if contents: try: return xml.dom.minidom.parseString(contents).encoding except ExpatError: pass @classmethod def _decode_page(cls, page): """Read the encoding (charset) of ``page`` and try to encode it using UTF-8.""" contents = page.read() encoding = cls._get_encoding(page, contents) or sys.getdefaultencoding() return unicode(contents, encoding=encoding).encode('utf-8') @classmethod def _get_first_text(cls, node, tag_names, strip=None): """Get the text value of the first child of ``node`` with tag ``tag_name``. The text is stripped using the value of ``strip``.""" if isinstance(tag_names, basestring): tag_names = [tag_names] if node: while tag_names: nodes = node.getElementsByTagName(tag_names.pop(0)) if nodes: child = nodes[0].firstChild return child and child.nodeValue.strip(strip) @classmethod def _join_filter(cls, sep, seq, pred=bool): """Join items in ``seq`` with string ``sep`` if pred(item) is True. Sequence items are passed to unicode() before joining.""" return sep.join([unicode(i) for i in seq if pred(i)]) class MediaWiki(WebGeocoder): def __init__(self, format_url, transform_string=None): """Initialize a geocoder that can parse MediaWiki pages with the GIS extension enabled. ``format_url`` is a URL string containing '%s' where the page name to request will be interpolated. For example: 'http://www.wiki.com/wiki/%s' ``transform_string`` is a callable that will make appropriate replacements to the input string before requesting the page. If None is given, the default transform_string which replaces ' ' with '_' will be used. It is recommended that you consider this argument keyword-only, since subclasses will likely place it last. """ self.format_url = format_url if callable(transform_string): self.transform_string = transform_string @classmethod def transform_string(cls, string): """Do the WikiMedia dance: replace spaces with underscores.""" return string.replace(' ', '_') def geocode(self, string): wiki_string = self.transform_string(string) url = self.format_url % wiki_string return self.geocode_url(url) def geocode_url(self, url): print "Fetching %s..." % url page = urlopen(url) name, (latitude, longitude) = self.parse_xhtml(page) return (name, (latitude, longitude)) def parse_xhtml(self, page): soup = isinstance(page, BeautifulSoup) and page or BeautifulSoup(page) meta = soup.head.find('meta', {'name': 'geo.placename'}) name = meta and meta['content'] or None meta = soup.head.find('meta', {'name': 'geo.position'}) if meta: position = meta['content'] latitude, longitude = util.parse_geo(position) if latitude == 0 or longitude == 0: latitude = longitude = None else: latitude = longitude = None return (name, (latitude, longitude)) class SemanticMediaWiki(MediaWiki): def __init__(self, format_url, attributes=None, relations=None, prefer_semantic=False, transform_string=None): """Initialize a geocoder that can parse MediaWiki pages with the GIS extension enabled, and can follow Semantic MediaWiki relations until a geocoded page is found. ``attributes`` is a sequence of semantic attribute names that can contain geographical coordinates. They will be tried, in order, if the page is not geocoded with the GIS extension. A single attribute may be passed as a string. For example: attributes=['geographical coordinate'] or: attributes='geographical coordinate' ``relations`` is a sequence of semantic relation names that will be followed, depth-first in order, until a geocoded page is found. A single relation name may be passed as a string. For example: relations=['Located in'] or: relations='Located in' ``prefer_semantic`` indicates whether or not the contents of the semantic attributes (given by ``attributes``) should be preferred over the GIS extension's coordinates if both exist. This defaults to False, since making it True will cause every page's RDF to be requested when it often won't be necessary. """ base = super(SemanticMediaWiki, self) base.__init__(format_url, transform_string) if attributes is None: self.attributes = [] elif isinstance(attributes, basestring): self.attributes = [attributes] else: self.attributes = attributes if relations is None: self.relations = [] elif isinstance(relations, basestring): self.relations = [relations] else: self.relations = relations self.prefer_semantic = prefer_semantic def transform_semantic(self, string): """Normalize semantic attribute and relation names by replacing spaces with underscores and capitalizing the result.""" return string.replace(' ', '_').capitalize() def geocode_url(self, url, tried=None): if tried is None: tried = set() print "Fetching %s..." % url page = urlopen(url) soup = BeautifulSoup(page) name, (latitude, longitude) = self.parse_xhtml(soup) if None in (name, latitude, longitude) or self.prefer_semantic: rdf_url = self.parse_rdf_link(soup) print "Fetching %s..." % rdf_url page = urlopen(rdf_url) things, thing = self.parse_rdf(page) name = self.get_label(thing) attributes = self.get_attributes(thing) for attribute, value in attributes: latitude, longitude = util.parse_geo(value) if None not in (latitude, longitude): break if None in (latitude, longitude): relations = self.get_relations(thing) for relation, resource in relations: url = things.get(resource, resource) if url in tried: # Avoid cyclic relationships. continue tried.add(url) name, (latitude, longitude) = self.geocode_url(url, tried) if None not in (name, latitude, longitude): break return (name, (latitude, longitude)) def parse_rdf_link(self, page, mime_type='application/rdf+xml'): """Parse the URL of the RDF link from the of ``page``.""" soup = isinstance(page, BeautifulSoup) and page or BeautifulSoup(page) link = soup.head.find('link', rel='alternate', type=mime_type) return link and link['href'] or None def parse_rdf(self, page): if not isinstance(page, basestring): page = self._decode_page(page) doc = xml.dom.minidom.parseString(page) things = {} for thing in reversed(doc.getElementsByTagName('smw:Thing')): name = thing.attributes['rdf:about'].value articles = thing.getElementsByTagName('smw:hasArticle') things[name] = articles[0].attributes['rdf:resource'].value # ``thing`` should now be the semantic data for the exported page. return (things, thing) def get_label(self, thing): return self._get_first_text(thing, 'rdfs:label') def get_attributes(self, thing, attributes=None): if attributes is None: attributes = self.attributes for attribute in attributes: attribute = self.transform_semantic(attribute) for node in thing.getElementsByTagName('attribute:' + attribute): value = node.firstChild.nodeValue.strip() yield (attribute, value) def get_relations(self, thing, relations=None): if relations is None: relations = self.relations for relation in relations: relation = self.transform_semantic(relation) for node in thing.getElementsByTagName('relation:' + relation): resource = node.attributes['rdf:resource'].value yield (relation, resource) class Google(WebGeocoder): """Geocoder using the Google Maps API.""" def __init__(self, api_key=None, domain='maps.google.com', resource='maps/geo', format_string='%s', output_format='kml'): """Initialize a customized Google geocoder with location-specific address information and your Google Maps API key. ``api_key`` should be a valid Google Maps API key. It is required for the 'maps/geo' resource to work. ``domain`` should be a the Google Maps domain to connect to. The default is 'maps.google.com', but if you're geocoding address in the UK (for example), you may want to set it to 'maps.google.co.uk'. ``resource`` is the HTTP resource to give the query parameter. 'maps/geo' is the HTTP geocoder and is a documented API resource. 'maps' is the actual Google Maps interface and its use for just geocoding is undocumented. Anything else probably won't work. ``format_string`` is a string containing '%s' where the string to geocode should be interpolated before querying the geocoder. For example: '%s, Mountain View, CA'. The default is just '%s'. ``output_format`` can be 'json', 'xml', 'kml', 'csv', or 'js' and will control the output format of Google's response. The default is 'kml' since it is supported by both the 'maps' and 'maps/geo' resources. The 'js' format is the most likely to break since it parses Google's JavaScript, which could change. However, it currently returns the best results for restricted geocoder areas such as the UK. """ self.api_key = api_key self.domain = domain self.resource = resource self.format_string = format_string self.output_format = output_format @property def url(self): domain = self.domain.strip('/') resource = self.resource.strip('/') return "http://%(domain)s/%(resource)s?%%s" % locals() def geocode(self, string, exactly_one=True): params = {'q': self.format_string % string, 'output': self.output_format.lower(), } if self.resource.rstrip('/').endswith('geo'): # An API key is only required for the HTTP geocoder. params['key'] = self.api_key url = self.url % urlencode(params) return self.geocode_url(url, exactly_one) def geocode_url(self, url, exactly_one=True): print "Fetching %s..." % url page = urlopen(url) dispatch = getattr(self, 'parse_' + self.output_format) return dispatch(page, exactly_one) def parse_xml(self, page, exactly_one=True): """Parse a location name, latitude, and longitude from an XML response. """ if not isinstance(page, basestring): page = self._decode_page(page) try: doc = xml.dom.minidom.parseString(page) except ExpatError: places = [] else: places = doc.getElementsByTagName('Placemark') if exactly_one and len(places) != 1: raise ValueError("Didn't find exactly one placemark! " \ "(Found %d.)" % len(places)) def parse_place(place): location = self._get_first_text(place, ['address', 'name']) or None points = place.getElementsByTagName('Point') point = points and points[0] or None coords = self._get_first_text(point, 'coordinates') or None if coords: longitude, latitude = [float(f) for f in coords.split(',')[:2]] else: latitude = longitude = None _, (latitude, longitude) = self.geocode(location) return (location, (latitude, longitude)) if exactly_one: return parse_place(places[0]) else: return (parse_place(place) for place in places) def parse_csv(self, page, exactly_one=True): raise NotImplementedError def parse_kml(self, page, exactly_one=True): return self.parse_xml(page, exactly_one) def parse_json(self, page, exactly_one=True): if not isinstance(page, basestring): page = self._decode_page(page) json = simplejson.loads(page) places = json.get('Placemark', []) if exactly_one and len(places) != 1: raise ValueError("Didn't find exactly one placemark! " \ "(Found %d.)" % len(places)) def parse_place(place): location = place.get('address') longitude, latitude = place['Point']['coordinates'][:2] return (location, (latitude, longitude)) if exactly_one: return parse_place(places[0]) else: return (parse_place(place) for place in places) def parse_js(self, page, exactly_one=True): """This parses JavaScript returned by queries the actual Google Maps interface and could thus break easily. However, this is desirable if the HTTP geocoder doesn't work for addresses in your country (the UK, for example). """ if not isinstance(page, basestring): page = self._decode_page(page) LATITUDE = r"[\s,]lat:\s*(?P-?\d+\.\d+)" LONGITUDE = r"[\s,]lng:\s*(?P-?\d+\.\d+)" LOCATION = r"[\s,]laddr:\s*'(?P.*?)(?.*?)(?:(?: \(.*?@)|$)" MARKER = '.*?'.join([LATITUDE, LONGITUDE, LOCATION]) MARKERS = r"{markers: (?P\[.*?\]),\s*polylines:" def parse_marker(marker): latitude, longitude, location = marker location = re.match(ADDRESS, location).group('address') latitude, longitude = float(latitude), float(longitude) return (location, (latitude, longitude)) match = re.search(MARKERS, page) markers = match and match.group('markers') or '' markers = re.findall(MARKER, markers) if exactly_one: if len(markers) != 1: raise ValueError("Didn't find exactly one marker! " \ "(Found %d.)" % len(markers)) marker = markers[0] return parse_marker(marker) else: return (parse_marker(marker) for marker in markers) class Yahoo(WebGeocoder): """Geocoder using the Yahoo! Maps API. Note: The Terms of Use dictate that the stand-alone geocoder may only be used for displaying Yahoo! Maps or points on Yahoo! Maps. Lame. See the Yahoo! Maps API Terms of Use for more information: http://developer.yahoo.com/maps/mapsTerms.html """ def __init__(self, app_id, format_string='%s', output_format='xml'): """Initialize a customized Yahoo! geocoder with location-specific address information and your Yahoo! Maps Application ID. ``app_id`` should be a valid Yahoo! Maps Application ID. ``format_string`` is a string containing '%s' where the string to geocode should be interpolated before querying the geocoder. For example: '%s, Mountain View, CA'. The default is just '%s'. ``output_format`` can currently only be 'xml'. """ self.app_id = app_id self.format_string = format_string self.output_format = output_format.lower() self.url = "http://api.local.yahoo.com/MapsService/V1/geocode?%s" def geocode(self, string, exactly_one=True): params = {'location': self.format_string % string, 'output': self.output_format, 'appid': self.app_id } url = self.url % urlencode(params) return self.geocode_url(url, exactly_one) def geocode_url(self, url, exactly_one=True): print "Fetching %s..." % url page = urlopen(url) parse = getattr(self, 'parse_' + self.output_format) return parse(page, exactly_one) def parse_xml(self, page, exactly_one=True): """Parse a location name, latitude, and longitude from an XML response. """ if not isinstance(page, basestring): page = self._decode_page(page) doc = xml.dom.minidom.parseString(page) results = doc.getElementsByTagName('Result') if exactly_one and len(results) != 1: raise ValueError("Didn't find exactly one result! " \ "(Found %d.)" % len(results)) def parse_result(result): strip = ", \n" address = self._get_first_text(result, 'Address', strip) city = self._get_first_text(result, 'City', strip) state = self._get_first_text(result, 'State', strip) zip = self._get_first_text(result, 'Zip', strip) country = self._get_first_text(result, 'Country', strip) city_state = self._join_filter(", ", [city, state]) place = self._join_filter(" ", [city_state, zip]) location = self._join_filter(", ", [address, place, country]) latitude = self._get_first_text(result, 'Latitude') or None latitude = latitude and float(latitude) longitude = self._get_first_text(result, 'Longitude') or None longitude = longitude and float(longitude) return (location, (latitude, longitude)) if exactly_one: return parse_result(results[0]) else: return (parse_result(result) for result in results) class GeocoderDotUS(WebGeocoder): """Geocoder using the United States-only geocoder.us API at http://geocoder.us. This geocoder is free for non-commercial purposes, otherwise you must register and pay per call. This class supports both free and commercial API usage. """ def __init__(self, username=None, password=None, format_string='%s', protocol='xmlrpc'): """Initialize a customized geocoder.us geocoder with location-specific address information and login information (for commercial usage). if ``username`` and ``password`` are given, they will be used to send account information to the geocoder.us API. If ``username`` is given and ``password`` is none, the ``getpass` module will be used to prompt for the password. ``format_string`` is a string containing '%s' where the string to geocode should be interpolated before querying the geocoder. For example: '%s, Mountain View, CA'. The default is just '%s'. ``protocol`` currently supports values of 'xmlrpc' and 'rest'. """ if username and password is None: prompt = "geocoder.us password for %r: " % username password = getpass.getpass(prompt) self.format_string = format_string self.protocol = protocol self.username = username self.__password = password @property def url(self): domain = "geocoder.us" username = self.username password = self.__password protocol = self.protocol.lower() if username and password: auth = "%s:%s@" % (username, password) resource = "member/service/%s/" % protocol else: auth = "" resource = "service/%s/" % protocol if protocol not in ['xmlrpc', 'soap']: resource += "geocode?%s" return "http://%(auth)s%(domain)s/%(resource)s" % locals() def geocode(self, string, exactly_one=True): dispatch = getattr(self, 'geocode_' + self.protocol) return dispatch(string, exactly_one) def geocode_xmlrpc(self, string, exactly_one=True): proxy = xmlrpclib.ServerProxy(self.url) results = proxy.geocode(self.format_string % string) if exactly_one and len(results) != 1: raise ValueError("Didn't find exactly one result! " \ "(Found %d.)" % len(results)) def parse_result(result): address = self._join_filter(" ", [result.get('number'), result.get('prefix'), result.get('street'), result.get('type'), result.get('suffix')]) city_state = self._join_filter(", ", [result.get('city'), result.get('state')]) place = self._join_filter(" ", [city_state, result.get('zip')]) location = self._join_filter(", ", [address, place]) or None latitude = result.get('lat') longitude = result.get('long') return (location, (latitude, longitude)) if exactly_one: return parse_result(results[0]) else: return (parse_result(result) for result in results) def geocode_rest(self, string, exactly_one=True): params = {'address': self.format_string % string} url = self.url % urlencode(params) page = urlopen(url) return self.parse_rdf(page, exactly_one) def parse_rdf(self, page, exactly_one=True): """Parse a location name, latitude, and longitude from an RDF response. """ if not isinstance(page, basestring): page = self._decode_page(page) doc = xml.dom.minidom.parseString(page) points = doc.getElementsByTagName('geo:Point') if exactly_one and len(points) != 1: raise ValueError("Didn't find exactly one point! " \ "(Found %d.)" % len(points)) def parse_point(point): strip = ", \n" location = self._get_first_text(point, 'dc:description', strip) location = location or None latitude = self._get_first_text(point, 'geo:lat') or None latitude = latitude and float(latitude) longitude = self._get_first_text(point, 'geo:long') or None longitude = longitude and float(longitude) return (location, (latitude, longitude)) if exactly_one: return parse_point(points[0]) else: return (parse_point(point) for point in points) class VirtualEarth(WebGeocoder): """Geocoder using Microsoft's Windows Live Local web service, powered by Virtual Earth. WARNING: This does not use a published API and can easily break if Microsoft changes their JavaScript. """ SINGLE_LOCATION = re.compile(r"AddLocation\((.*?')\)") AMBIGUOUS_LOCATION = re.compile(r"UpdateAmbiguousList\(\[(.*?)\]\)") AMBIGUOUS_SPLIT = re.compile(r"\s*,?\s*new Array\(") STRING_QUOTE = re.compile(r"(?-?\d+\.?\d*)%s?" % DEGREE arcmin = r"((?P<%%s_minutes>\d+\.?\d*)[m'%s])?" % ARCMIN arcsec = r'((?P<%%s_seconds>\d+\.?\d*)[s"%s])?' % ARCSEC coord_lat = r"\s*".join([coord % 'latitude', arcmin % 'latitude', arcsec % 'latitude']) coord_lng = r"\s*".join([coord % 'longitude', arcmin % 'longitude', arcsec % 'longitude']) direction_lat = r"(?P[NS])?" direction_lng = r"(?P[EW])?" lat = r"\s*".join([coord_lat, direction_lat]) lng = r"\s*".join([coord_lng, direction_lng]) regex = sep.join([lat, lng]) match = re.match(regex, string) if match: d = match.groupdict() lat = d.get('latitude_degrees') lng = d.get('longitude_degrees') if lat: lat = float(lat) lat += arc_degrees(d.get('latitude_minutes', 0), d.get('latitude_seconds', 0)) n_s = d.get('north_south', 'N').upper() if n_s == 'S': lat *= -1 if lng: lng = float(lng) lng += arc_degrees(d.get('longitude_minutes', 0), d.get('longitude_seconds', 0)) e_w = d.get('east_west', 'E').upper() if e_w == 'W': lng *= -1 return (lat, lng) else: return (None, None) d-rats-0.3.3/d_rats/gps.py000066400000000000000000001003741160617671700153470ustar00rootroot00000000000000import re import time import tempfile import platform import datetime import subst import threading import serial import socket from math import pi,cos,acos,sin,atan2 import utils if __name__ == "__main__": import gettext gettext.install("D-RATS") TEST = "$GPGGA,180718.02,4531.3740,N,12255.4599,W,1,07,1.4,50.6,M,-21.4,M,,*63 KE7JSS ,440.350+ PL127.3" EARTH_RADIUS = 3963.1 EARTH_UNITS = "mi" DEGREE = u"\u00b0" DPRS_TO_APRS = {} # The DPRS to APRS mapping is pretty horrific, but the following # attempts to create a mapping based on looking at the javascript # for DPRSCalc and a list of regular APRS symbols # # http://ham-shack.com/aprs_pri_symbols.html # http://www.aprs-is.net/DPRSCalc.aspx for i in range(0, 26): asciival = ord("A") + i char = chr(asciival) pri = "/" sec = "\\" DPRS_TO_APRS["P%s" % char] = pri + char DPRS_TO_APRS["L%s" % char] = pri + char.lower() DPRS_TO_APRS["A%s" % char] = sec + char DPRS_TO_APRS["S%s" % char] = sec + char.lower() if i <= 15: pchar = chr(ord(" ") + i) DPRS_TO_APRS["B%s" % char] = pri + pchar DPRS_TO_APRS["O%s" % char] = sec + pchar elif i >= 17: pchar = chr(ord(" ") + i + 9) DPRS_TO_APRS["M%s" % char] = pri + pchar DPRS_TO_APRS["N%s" % char] = sec + pchar if i <= 5: char = chr(ord("S") + i) pchar = chr(ord("[") + i) DPRS_TO_APRS["H%s" % char] = pri + pchar DPRS_TO_APRS["D%s" % char] = sec + pchar #for k in sorted(DPRS_TO_APRS.keys()): # print "%s => %s" % (k, DPRS_TO_APRS[k]) APRS_TO_DPRS = {} for k,v in DPRS_TO_APRS.items(): APRS_TO_DPRS[v] = k def dprs_to_aprs(symbol): if len(symbol) < 2: print "Invalid DPRS symbol: `%s'" % symbol return None else: return DPRS_TO_APRS.get(symbol[0:2], None) def parse_dms(string): string = string.replace(u"\u00b0", " ") string = string.replace('"', ' ') string = string.replace("'", ' ') string = string.replace(' ', ' ') string = string.strip() try: (d, m, s) = string.split(' ', 3) deg = int(d) min = int(m) sec = float(s) except Exception, e: deg = min = sec = 0 if deg < 0: mul = -1 else: mul = 1 deg = abs(deg) return (deg + (min / 60.0) + (sec / 3600.0)) * mul def set_units(units): global EARTH_RADIUS global EARTH_UNITS if units == _("Imperial"): EARTH_RADIUS = 3963.1 EARTH_UNITS = "mi" elif units == _("Metric"): EARTH_RADIUS = 6380.0 EARTH_UNITS = "km" print "Set GPS units to %s" % units def value_with_units(value): if value < 0.5: if EARTH_UNITS == "km": scale = 1000 units = "m" elif EARTH_UNITS == "mi": scale = 5280 units = "ft" else: scale = 1 units = EARTH_UNITS else: scale = 1 units = EARTH_UNITS return "%.2f %s" % (value * scale, units) def NMEA_checksum(string): checksum = 0 for i in string: checksum ^= ord(i) return "*%02x" % checksum def GPSA_checksum(string): def calc(buf): icomcrc = 0xffff for _char in buf: char = ord(_char) for i in range(0, 8): xorflag = (((icomcrc ^ char) & 0x01) == 0x01) icomcrc = (icomcrc >> 1) & 0x7fff if xorflag: icomcrc ^= 0x8408 char = (char >> 1) & 0x7f return (~icomcrc) & 0xffff return calc(string) def DPRS_checksum(callsign, msg): csum = 0 string = "%-8s,%s" % (callsign, msg) for i in string: csum ^= ord(i) return "*%02X" % csum def deg2rad(deg): return deg * (pi / 180) def rad2deg(rad): return rad / (pi / 180) def dm2deg(deg, min): return deg + (min / 60.0) def deg2dm(decdeg): deg = int(decdeg) min = (decdeg - deg) * 60.0 return deg, min def nmea2deg(nmea, dir="N"): deg = int(nmea) / 100 try: min = nmea % (deg * 100) except ZeroDivisionError, e: min = int(nmea) if dir == "S" or dir == "W": m = -1 else: m = 1 return dm2deg(deg, min) * m def deg2nmea(deg): deg, min = deg2dm(deg) return (deg * 100) + min def meters2feet(meters): return meters * 3.2808399 def feet2meters(feet): return feet * 0.3048 def distance(lat_a, lon_a, lat_b, lon_b): lat_a = deg2rad(lat_a) lon_a = deg2rad(lon_a) lat_b = deg2rad(lat_b) lon_b = deg2rad(lon_b) earth_radius = EARTH_RADIUS #print "cos(La)=%f cos(la)=%f" % (cos(lat_a), cos(lon_a)) #print "cos(Lb)=%f cos(lb)=%f" % (cos(lat_b), cos(lon_b)) #print "sin(la)=%f" % sin(lon_a) #print "sin(lb)=%f" % sin(lon_b) #print "sin(La)=%f sin(Lb)=%f" % (sin(lat_a), sin(lat_b)) #print "cos(lat_a) * cos(lon_a) * cos(lat_b) * cos(lon_b) = %f" % (\ # cos(lat_a) * cos(lon_a) * cos(lat_b) * cos(lon_b)) #print "cos(lat_a) * sin(lon_a) * cos(lat_b) * sin(lon_b) = %f" % (\ # cos(lat_a) * sin(lon_a) * cos(lat_b) * sin(lon_b)) #print "sin(lat_a) * sin(lat_b) = %f" % (sin(lat_a) * sin(lat_b)) tmp = (cos(lat_a) * cos(lon_a) * \ cos(lat_b) * cos(lon_b)) + \ (cos(lat_a) * sin(lon_a) * \ cos(lat_b) * sin(lon_b)) + \ (sin(lat_a) * sin(lat_b)) # Correct round-off error (which is just *silly*) if tmp > 1: tmp = 1 elif tmp < -1: tmp = -1 distance = acos(tmp) return distance * earth_radius def parse_date(string, fmt): try: return datetime.datetime.strptime(string, fmt) except AttributeError, e: print "Enabling strptime() workaround for Python <= 2.4.x" vals = {} for c in "mdyHMS": i = fmt.index(c) vals[c] = int(string[i-1:i+1]) if len(vals.keys()) != (len(fmt) / 2): raise Exception("Not all date bits converted") return datetime.datetime(vals["y"] + 2000, vals["m"], vals["d"], vals["H"], vals["M"], vals["S"]) class GPSPosition(object): """Represents a position on the globe, either from GPS data or a static positition""" def _from_coords(self, lat, lon, alt=0): try: self.latitude = float(lat) except ValueError: self.latitude = parse_dms(lat) try: self.longitude = float(lon) except ValueError: self.longitude = parse_dms(lon) self.altitude = float(alt) self.satellites = 3 self.valid = True def _parse_dprs_comment(self): symbol = self.comment[0:4].strip() astidx = self.comment.rindex("*") checksum = self.comment[astidx:] _checksum = DPRS_checksum(self.station, self.comment[:astidx]) if int(_checksum[1:], 16) != int(checksum[1:], 16): print "CHECKSUM(%s): %s != %s" % (self.station, int(_checksum[1:], 16), int(checksum[1:], 16)) #print "Failed to parse DPRS comment:" #print " Comment: |%s|" % self.comment #print " Check: %s %s (%i)" % (checksum, _checksum, astidx) raise Exception("DPRS checksum failed") self.APRSIcon = dprs_to_aprs(symbol) self.comment = self.comment[4:astidx].strip() def __init__(self, lat=0, lon=0, station="UNKNOWN"): self.valid = False self.altitude = 0 self.satellites = 0 self.station = station self.comment = "" self.current = None self.date = datetime.datetime.now() self.speed = None self.direction = None self.APRSIcon = None self._original_comment = "" self._from_coords(lat, lon) def __iadd__(self, update): self.station = update.station if not update.valid: return self if update.satellites: self.satellites = update.satellites if update.altitude: self.altitude = update.altitude self.latitude = update.latitude self.longitude = update.longitude self.date = update.date if update.speed: self.speed = update.speed if update.direction: self.direction = update.direction if update.comment: self.comment = update.comment self._original_comment = update._original_comment if update.APRSIcon: self.APRSIcon = update.APRSIcon return self def __str__(self): if self.valid: if self.current: dist = self.distance_from(self.current) bear = self.current.bearing_to(self) distance = " - %.1f %s " % (dist, EARTH_UNITS) + \ _("away") + \ " @ %.1f " % bear + \ _("degrees") else: distance = "" if self.comment: comment = " (%s)" % self.comment else: comment = "" if self.speed and self.direction: if EARTH_UNITS == "mi": speed = "%.1f mph" % (float(self.speed) * 1.15077945) elif EARTH_UNITS == "m": speed = "%.1f km/h" % (float(self.speed) * 1.852) else: speed = "%.2f knots" % float(self.speed) dir = " (" + _("Heading") +" %.0f at %s)" % (self.direction, speed) else: dir = "" if EARTH_UNITS == "mi": alt = "%i ft" % meters2feet(self.altitude) else: alt = "%i m" % self.altitude return "%s " % self.station + \ _("reporting") + \ " %.4f,%.4f@%s at %s%s%s%s" % ( \ self.latitude, self.longitude, alt, self.date.strftime("%H:%M:%S"), subst.subst_string(comment), distance, dir) else: return "(" + _("Invalid GPS data") + ")" def _NMEA_format(self, val, latitude): if latitude: if val > 0: d = "N" else: d = "S" else: if val > 0: d = "E" else: d = "W" return "%.3f,%s" % (deg2nmea(abs(val)), d) def station_format(self): if " " in self.station: call, extra = self.station.split(" ", 1) sta = "%-7.7s%1.1s" % (call.strip(), extra.strip()) else: sta = self.station return sta def to_NMEA_GGA(self, ssid=" "): """Returns an NMEA-compliant GPGGA sentence""" date = time.strftime("%H%M%S") lat = self._NMEA_format(self.latitude, True) lon = self._NMEA_format(self.longitude, False) data = "GPGGA,%s,%s,%s,1,%i,0,%i,M,0,M,," % ( \ date, lat, lon, self.satellites, self.altitude) sta = self.station_format() # If we had an original comment (with some encoding), use that instead if self._original_comment: com = self._original_comment else: com = self.comment return "$%s%s\r\n%-8.8s,%-20.20s\r\n" % (data, NMEA_checksum(data), sta, com) def to_NMEA_RMC(self): """Returns an NMEA-compliant GPRMC sentence""" tstamp = time.strftime("%H%M%S") dstamp = time.strftime("%d%m%y") lat = self._NMEA_format(self.latitude, True) lon = self._NMEA_format(self.longitude, False) if self.speed: speed = "%03.1f" % self.speed else: speed = "000.0" if self.direction: dir = "%03.1f" % self.direction else: dir = "000.0" data = "GPRMC,%s,A,%s,%s,%s,%s,%s,000.0,W" % ( \ tstamp, lat, lon, speed, dir, dstamp) sta = self.station_format() return "$%s%s\r\n%-8.8s,%-20.20s\r\n" % (data, NMEA_checksum(data), sta, self.comment) def to_APRS(self, dest="APRATS", symtab="/", symbol=">"): """Returns a GPS-A (APRS-compliant) string""" stamp = time.strftime("%H%M%S", time.gmtime()) if " " in self.station: sta = self.station.replace(" ", "-") else: sta = self.station s = "%s>%s,DSTAR*:@%sh" % (sta, dest, stamp) if self.latitude > 0: ns = "N" Lm = 1 else: ns = "S" Lm = -1 if self.longitude > 0: ew = "E" lm = 1 else: ew = "W" lm = -1 s += "%.2f%s%s%08.2f%s%s" % (deg2nmea(self.latitude * Lm), ns, symtab, deg2nmea(self.longitude * lm), ew, symbol) if self.speed and self.direction: s += "%.1f/%.1f" % (float(self.speed), float(self.direction)) if self.comment: l = 43 if self.altitude: l -= len("/A=xxxxxx") s += "%s" % self.comment[:l] if self.altitude: s += "%s/A=%06i" % (self.comment and " " or "", meters2feet(float(self.altitude))) s += "\r" return "$$CRC%04X,%s\n" % (GPSA_checksum(s), s) def set_station(self, station, comment="D-RATS"): self.station = station self.comment = comment self._original_comment = comment if len(self.comment) >=7 and "*" in self.comment[-3:-1]: self._parse_dprs_comment() def distance_from(self, pos): return distance(self.latitude, self.longitude, pos.latitude, pos.longitude) def bearing_to(self, pos): lat_me = deg2rad(self.latitude) lon_me = deg2rad(self.longitude) lat_u = deg2rad(pos.latitude) lon_u = deg2rad(pos.longitude) lat_d = deg2rad(pos.latitude - self.latitude) lon_d = deg2rad(pos.longitude - self.longitude) y = sin(lon_d) * cos(lat_u) x = cos(lat_me) * sin(lat_u) - \ sin(lat_me) * cos(lat_u) * cos(lon_d) bearing = rad2deg(atan2(y, x)) return (bearing + 360) % 360 def set_relative_to_current(self, current): self.current = current def coordinates(self): return "%.4f,%.4f" % (self.latitude, self.longitude) def fuzzy_to(self, pos): dir = self.bearing_to(pos) dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] delta = 22.5 angle = 0 direction = "?" for i in dirs: if dir > angle and dir < (angle + delta): direction = i angle += delta return "%.1f %s %s" % (self.distance_from(pos), EARTH_UNITS, direction) class NMEAGPSPosition(GPSPosition): """A GPSPosition initialized from a NMEA sentence""" def _test_checksum(self, string, csum): try: idx = string.index("*") except: print "String does not contain '*XY' checksum" return False segment = string[1:idx] csum = csum.upper() _csum = NMEA_checksum(segment).upper() if csum != _csum: print "Failed checksum: %s != %s" % (csum, _csum) return csum == _csum def _parse_GPGGA(self, string): elements = string.split(",", 14) if len(elements) < 15: raise Exception("Unable to split GPGGA" % len(elements)) t = time.strftime("%m%d%y") + elements[1] if "." in t: t = t.split(".")[0] self.date = parse_date(t, "%m%d%y%H%M%S") self.latitude = nmea2deg(float(elements[2]), elements[3]) self.longitude = nmea2deg(float(elements[4]), elements[5]) print "%f,%f" % (self.latitude, self.longitude) self.satellites = int(elements[7]) self.altitude = float(elements[9]) m = re.match("^([0-9]*)(\*[A-z0-9]{2})\r?\n?(.*)$", elements[14]) if not m: raise Exception("No checksum (%s)" % elements[14]) csum = m.group(2) if "," in m.group(3): sta, com = m.group(3).split(",", 1) if not sta.strip().startswith("$"): self.station = utils.filter_to_ascii(sta.strip()[0:8]) self.comment = utils.filter_to_ascii(com.strip()[0:20]) self._original_comment = self.comment if len(self.comment) >=7 and "*" in self.comment[-3:-1]: self._parse_dprs_comment() self.valid = self._test_checksum(string, csum) def _parse_GPRMC(self, string): if "\r\n" in string: nmea, station = string.split("\r\n", 1) else: nmea = string station = "" elements = nmea.split(",", 12) if len(elements) < 12: raise Exception("Unable to split GPRMC (%i)" % len(elements)) t = elements[1] d = elements[9] if "." in t: t = t.split(".", 2)[0] self.date = parse_date(d+t, "%d%m%y%H%M%S") self.latitude = nmea2deg(float(elements[3]), elements[4]) self.longitude = nmea2deg(float(elements[5]), elements[6]) self.speed = float(elements[7]) self.direction = float(elements[8]) if "*" in elements[11]: end = 11 # NMEA <=2.0 elif "*" in elements[12]: end = 12 # NMEA 2.3 else: raise Exception("GPRMC has no checksum in 12 or 13") m = re.match("^.?(\*[A-z0-9]{2})", elements[end]) if not m: print "Invalid end: %s" % elements[end] return csum = m.group(1) if "," in station: sta, com = station.split(",", 1) self.station = utils.filter_to_ascii(sta.strip()) self.comment = utils.filter_to_ascii(com.strip()) self._original_comment = self.comment if len(self.comment) >= 7 and "*" in self.comment[-3:-1]: self._parse_dprs_comment() if elements[2] != "A": self.valid = False print "GPRMC marked invalid by GPS (%s)" % elements[2] else: print "GPRMC is valid" self.valid = self._test_checksum(string, csum) def _from_NMEA_GPGGA(self, string): string = string.replace('\r', ' ') string = string.replace('\n', ' ') try: self._parse_GPGGA(string) except Exception, e: import traceback import sys traceback.print_exc(file=sys.stdout) print "Invalid GPS data: %s" % e self.valid = False def _from_NMEA_GPRMC(self, string): try: self._parse_GPRMC(string) except Exception, e: import traceback import sys traceback.print_exc(file=sys.stdout) print "Invalid GPS data: %s" % e self.valid = False def __init__(self, sentence, station=_("UNKNOWN")): GPSPosition.__init__(self) if sentence.startswith("$GPGGA"): self._from_NMEA_GPGGA(sentence) elif sentence.startswith("$GPRMC"): self._from_NMEA_GPRMC(sentence) else: print "Unsupported GPS sentence type: %s" % sentence class APRSGPSPosition(GPSPosition): def _parse_date(self, string): prefix = string[0] suffix = string[-1] digits = string[1:-1] if suffix == "z": ds = digits[0:2] + \ time.strftime("%m%y", time.gmtime()) + \ digits[2:] + "00" elif suffix == "/": ds = digits[0:2] + time.strftime("%m%y") + digits[2:] + "00" elif suffix == "h": ds = time.strftime("%d%m%y", time.gmtime()) + digits else: print "Unknown APRS date suffix: `%s'" % suffix return datetime.datetime.now() d = parse_date(ds, "%d%m%y%H%M%S") if suffix in "zh": delta = datetime.datetime.utcnow() - datetime.datetime.now() else: delta = datetime.timedelta(0) return d - delta def _parse_GPSA(self, string): m = re.match("^\$\$CRC([A-Z0-9]{4}),(.*)$", string) if not m: return crc = m.group(1) _crc = "%04X" % GPSA_checksum(m.group(2)) if crc != _crc: print "APRS CRC mismatch: %s != %s (%s)" % (crc, _crc, m.group(2)) return elements = string.split(",") if not elements[0].startswith("$$CRC"): print "Missing $$CRC..." return self.station, dst = elements[1].split(">") path, data = elements[2].split(":") # 1 = Entire stamp or ! or = # 2 = stamp prefix # 3 = stamp suffix # 4 = latitude # 5 = N/S # 6 = symbol table # 7 = longitude # 8 = E/W # 9 = symbol #10 = comment #11 = altitude string expr = "^(([@/])[0-9]{6}([/hz])|!|=)" + \ "([0-9]{4}\.[0-9]{2})([NS])(.)?" + \ "([0-9]{5}\.[0-9]{2})([EW])(.)" + \ "([^/]*)(/A=[0-9]{6})?" m = re.search(expr, data) if not m: print "Did not match GPS-A: `%s'" % data return if m.group(1) in "!=": self.date = datetime.datetime.now() elif m.group(2) in "@/": self.date = self._parse_date(m.group(1)) else: print "Unknown timestamp prefix: %s" % m.group(1) self.date = datetime.datetime.now() self.latitude = nmea2deg(float(m.group(4)), m.group(5)) self.longitude = nmea2deg(float(m.group(7)), m.group(8)) self.comment = m.group(10).strip() self._original_comment = self.comment if len(m.groups()) == 11 and m.group(11): _, alt = m.group(11).split("=") self.altitude = feet2meters(int(alt)) self.valid = True def _from_APRS(self, string): self.valid = False try: self._parse_GPSA(string) except Exception, e: print "Invalid APRS: %s" % e return False return self.valid def __init__(self, message): GPSPosition.__init__(self) self._from_APRS(message) class MapImage(object): def __init__(self, center): self.key = "ABQIAAAAWot3KuWpenfCAGfQ65FdzRTaP0xjRaMPpcw6bBbU2QUEXQBgHBR5Rr2HTGXYVWkcBFNkPvxtqV4VLg" self.center = center self.markers = [center] def add_markers(self, markers): self.markers += markers def get_image_url(self): el = [ "key=%s" % self.key, "center=%s" % self.center.coordinates(), "size=400x400"] mstr = "markers=" index = ord("a") for m in self.markers: mstr += "%s,blue%s|" % (m.coordinates(), chr(index)) index += 1 el.append(mstr) return "http://maps.google.com/staticmap?%s" % ("&".join(el)) def station_table(self): table = "" index = ord('A') for m in self.markers: table += "%s%s%s\n" % (\ chr(index), m.station, m.coordinates()) index += 1 return table def make_html(self): return """ Known stations

Known Stations



%s
""" % (self.get_image_url(), self.station_table()) def display_in_browser(self): f = tempfile.NamedTemporaryFile(suffix=".html") name = f.name f.close() f = file(name, "w") f.write(self.make_html()) f.flush() f.close() p = platform.get_platform() p.open_html_file(f.name) class GPSSource(object): def __init__(self, port, rate=4800): self.port = port self.enabled = False self.broken = None try: self.serial = serial.Serial(port=port, baudrate=rate, timeout=1) except Exception, e: print "Unable to open port `%s': %s" % (port, e) self.broken = _("Unable to open GPS port") self.thread = None self.last_valid = False self.position = GPSPosition() def start(self): if self.broken: print "Not starting broken GPSSource" return self.invalid = 100 self.enabled = True self.thread = threading.Thread(target=self.gpsthread) self.thread.setDaemon(True) self.thread.start() def stop(self): if self.thread and self.enabled: self.enabled = False self.thread.join() self.serial.close() def gpsthread(self): while self.enabled: data = self.serial.read(1024) lines = data.split("\r\n") for line in lines: if line.startswith("$GPGGA") or \ line.startswith("$GPRMC"): position = NMEAGPSPosition(line) if position.valid and line.startswith("$GPRMC"): self.invalid = 0 elif self.invalid < 10: self.invalid += 1 if position.valid and self.position.valid: self.position += position print _("ME") + ": %s" % self.position elif position.valid: self.position = position else: print "Could not parse: %s" % line def get_position(self): return self.position def status_string(self): if self.broken: return self.broken elif self.invalid < 10 and self.position.satellites >= 3: return _("GPS Locked") + " (%i sats)" % self.position.satellites else: return _("GPS Not Locked") class NetworkGPSSource(GPSSource): def __init__(self, port): self.port = port self.enabled = False self.thread = None self.position = GPSPosition() self.last_valid = False self.sock = None self.broken = None def start(self): self.enabled = True self.thread = threading.Thread(target=self.gpsthread) self.thread.setDaemon(True) self.thread.start() def stop(self): if self.thread and self.enabled: self.enabled = False self.thread.join() def connect(self): try: _, host, port = self.port.split(":", 3) port = int(port) except ValueError, e: print "Unable to parse %s (%s)" % (self.port, e) self.broken = _("Unable to parse address") return False print "Connecting to %s:%i" % (host, port) try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) self.sock.settimeout(10) except Exception, e: print "Unable to connect: %s" % e self.broken = _("Unable to connect") + ": %s" % e self.sock = None return False self.sock.send("r\n") return True def gpsthread(self): while self.enabled: if not self.sock: if not self.connect(): time.sleep(1) continue try: data = self.sock.recv(1024) except Exception, e: self.sock.close() self.sock = None print _("GPSd Socket closed") continue line = data.strip() if not (line.startswith("$GPGGA") or \ line.startswith("$GPRMC")): continue pos = NMEAGPSPosition(line) self.last_valid = pos.valid if pos.valid and self.position.valid: self.position += pos elif pos.valid: self.position = pos else: print "Could not parse: %s" % line def get_position(self): return self.position def status_string(self): if self.broken: return self.broken elif self.last_valid and self.position.satellites >= 3: return _("GPSd Locked") + " (%i sats)" % self.position.satellites else: return _("GPSd Not Locked") class StaticGPSSource(GPSSource): def __init__(self, lat, lon, alt=0): self.lat = lat self.lon = lon self.alt = alt self.position = GPSPosition(self.lat, self.lon) self.position.altitude = int(float(alt)) if EARTH_UNITS == "mi": # This is kinda ugly, but assume we're given altitude in the same # type of units as we've been asked to display self.position.altitude = feet2meters(self.position.altitude) def start(self): pass def stop(self): pass def get_position(self): return self.position def status_string(self): return _("Static position") def parse_GPS(string): fixes = [] while "$" in string: try: if "$GPGGA" in string: fixes.append(NMEAGPSPosition(string[string.index("$GPGGA"):])) string = string[string.index("$GPGGA")+6:] elif "$GPRMC" in string: fixes.append(NMEAGPSPosition(string[string.index("$GPRMC"):])) string = string[string.index("$GPRMC")+6:] elif "$$CRC" in string: return APRSGPSPosition(string[string.index("$$CRC"):]) else: string = string[string.index("$")+1:] except Exception, e: print "Exception during GPS parse: %s" % e string = string[string.index("$")+1:] if not fixes: return None fix = fixes[0] fixes = fixes[1:] for extra in fixes: print "Appending fix: %s" % extra fix += extra return fix if __name__ == "__main__": nmea_strings = [ "$GPRMC,010922,A,4603.6695,N,07307.3033,W,0.6,66.8,060508,16.1,W,A*1D\r\nVE2SE 9,MV VE2SE@RAC.CA*32", "$GPGGA,203008.78,4524.9729,N,12246.9580,W,1,03,3.8,00133,M,,,,*39", "$GPGGA,183324.518,4533.0875,N,12254.5939,W,2,04,3.4,48.6,M,-19.6,M,1.2,0000*74", "$GPRMC,215348,A,4529.3672,N,12253.2060,W,0.0,353.8,030508,17.5,E,D*3C", "$GPGGA,075519,4531.254,N,12259.400,W,1,3,0,0.0,M,0,M,,*55\r\nK7HIO ,GPS Info", "$GPRMC,074919.04,A,4524.9698,N,12246.9520,W,00.0,000.0,260508,19.,E*79", "$GPRMC,123449.089,A,3405.1123,N,08436.4301,W,000.0,000.0,021208,,,A*71", "$GPRMC,123449.089,A,3405.1123,N,08436.4301,W,000.0,000.0,021208,,,A*71\r\nKK7DS M,LJ DAN*C", "$GPRMC,230710,A,2748.1414,N,08238.5556,W,000.0,033.1,111208,004.3,W*77", ] print "-- NMEA --" for s in nmea_strings: p = NMEAGPSPosition(s) if p.valid: print "Pass: %s" % str(p) else: print "** FAIL: %s" % s aprs_strings = [ "$$CRCCE3E,AE5PL-T>API282,DSTAR*:!3302.39N/09644.66W>/\r", "$$CRC1F72,KI4IFW-1>APRATS,DSTAR*:@291930/4531.50N/12254.98W>APRS test beacon /A=000022", "$$CRC80C3,VA2PBI>APU25N,DSTAR*:=4539.33N/07330.28W-73 de Pierre D-Star Montreal {UIV32N}", "$$CRCA31F,VA2PBI>API282,DSTAR*:/221812z4526.56N07302.34W/\r", ] print "\n-- GPS-A --" for s in aprs_strings: p = APRSGPSPosition(s) if p.valid: print "Pass: %s" % str(p) else: print "** FAIL: %s" % s d-rats-0.3.3/d_rats/image.py000066400000000000000000000063161160617671700156410ustar00rootroot00000000000000import gtk import tempfile import os import inputdialog import miscwidgets import platform sizes = [ "160x120", "320x240", "640x480", "1024x768", "Original Size", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", ] def has_image_support(): global IMAGE try: import Image except ImportError: return False IMAGE = Image return True def update_image(filename, dlg): reqsize = dlg.size.get_active_text() if "x" in reqsize: _h, _w = reqsize.split("x") h = int(_h) w = int(_w) elif "%" in reqsize: factor = float(reqsize[0:2]) / 100.0 h, w = dlg.image.size w = int(w * factor) h = int(h * factor) else: h, w = dlg.image.size resized = dlg.image.resize((h, w)) (base, ext) = os.path.splitext(os.path.basename(filename)) dlg.resized = os.path.join(tempfile.gettempdir(), "resized_" + base + ".jpg") resized.save(dlg.resized, quality=dlg.quality) print "Saved to %s" % dlg.resized f = file(dlg.resized) f.seek(0, 2) size = f.tell() f.close() dlg.sizelabel.set_text("%i KB" % (size >> 10)) dlg.preview.set_from_file(dlg.resized) def set_quality(scale, event, value, dlg): dlg.quality = int(value) dlg.update() def build_image_dialog(filename, image, dlgParent=None): d = inputdialog.FieldDialog(title="Send Image", parent=dlgParent) def update(): update_image(filename, d) d.add_field(_("Filename"), gtk.Label(os.path.basename(filename))) d.sizelabel = gtk.Label("--") d.add_field(_("Size"), d.sizelabel) d.size = miscwidgets.make_choice(sizes, False, sizes[1]) d.size.connect("changed", lambda x: update()) d.add_field(_("Resize to"), d.size) quality = gtk.HScale(gtk.Adjustment(50, 1, 100, 10, 10)) quality.connect("format-value", lambda s,v: "%i" % v) quality.connect("change-value", set_quality, d) d.add_field(_("Quality"), quality) d.preview = gtk.Image() d.preview.show() sw = gtk.ScrolledWindow() sw.add_with_viewport(d.preview) sw.set_size_request(320,320) d.add_field(_("Preview"), sw, full=True) d.set_size_request(400, 450) d.image = image d.resized = None d.quality = 50 d.update = update d.update() return d def send_image(fn, dlgParent=None): if not has_image_support(): msg = _("No support for resizing images. Send unaltered?") if main_common.ask_for_confirmation(msg, dlgParent): return fn else: return None try: img = IMAGE.open(fn) except IOError, e: print "%s: %s" % (fn, e) d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=dlgParent) d.set_property("text", _("Unknown image type")) d.run() d.destroy() return d = build_image_dialog(fn, img, dlgParent) r = d.run() f = d.resized d.destroy() if r == gtk.RESPONSE_OK: return f else: return None if __name__ == "__main__": has_image_support() print send_image() gtk.main() d-rats-0.3.3/d_rats/inputdialog.py000066400000000000000000000103021160617671700170640ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk from miscwidgets import make_choice class TextInputDialog(gtk.Dialog): def respond_ok(self, *args): self.response(gtk.RESPONSE_OK) def __init__(self, **args): buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK) gtk.Dialog.__init__(self, buttons=buttons, **args) self.label = gtk.Label() self.label.set_size_request(300, 100) # pylint: disable-msg=E1101 self.vbox.pack_start(self.label, 1, 1, 0) self.text = gtk.Entry() self.text.connect("activate", self.respond_ok, None) # pylint: disable-msg=E1101 self.vbox.pack_start(self.text, 1, 1, 0) self.label.show() self.text.show() class ChoiceDialog(gtk.Dialog): editable = False def __init__(self, choices, **args): buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK) gtk.Dialog.__init__(self, buttons=buttons, **args) self.label = gtk.Label() self.label.set_size_request(300, 100) # pylint: disable-msg=E1101 self.vbox.pack_start(self.label, 1, 1, 0) self.label.show() try: default = choices[0] except IndexError: default = None self.choice = make_choice(sorted(choices), self.editable, default) # pylint: disable-msg=E1101 self.vbox.pack_start(self.choice, 1, 1, 0) self.choice.show() self.set_default_response(gtk.RESPONSE_OK) class EditableChoiceDialog(ChoiceDialog): editable = True def __init__(self, choices, **args): ChoiceDialog.__init__(self, choices, **args) self.choice.child.set_activates_default(True) class ExceptionDialog(gtk.MessageDialog): def __init__(self, exception, **args): gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK, **args) self.set_property("text", _("An error has occurred")) self.format_secondary_text(str(exception)) class FieldDialog(gtk.Dialog): def __init__(self, **kwargs): if "buttons" not in kwargs.keys(): kwargs["buttons"] = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) self.__fields = {} gtk.Dialog.__init__(self, **kwargs) self.set_default_response(gtk.RESPONSE_OK) self.set_modal(True) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) def response(self, _): print "Blocking response" return def add_field(self, label, widget, validator=None, full=False): if full: box = gtk.VBox(False, 2) else: box = gtk.HBox(True, 2) lab = gtk.Label(label) lab.show() widget.set_size_request(150, -1) widget.show() box.pack_start(lab, 0, 0, 0) if full: box.pack_start(widget, 1, 1, 1) else: box.pack_start(widget, 0, 0, 0) box.show() # pylint: disable-msg=E1101 if full: self.vbox.pack_start(box, 1, 1, 1) else: self.vbox.pack_start(box, 0, 0, 0) self.__fields[label] = widget def get_field(self, label): return self.__fields.get(label, None) if __name__ == "__main__": # pylint: disable-msg=C0103 d = FieldDialog(buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK)) d.add_field("Foo", gtk.Entry()) d.add_field("Bar", make_choice(["A", "B"])) d.run() gtk.main() d.destroy() d-rats-0.3.3/d_rats/mailsrv.py000066400000000000000000000226331160617671700162340ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2010 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os from glob import glob import SocketServer import threading import smtpd import asyncore import email import random import time import re from d_rats import utils from d_rats import msgrouting from d_rats import emailgw def mkmsgid(callsign): r = random.SystemRandom().randint(0,100000) return "%s.%x.%x" % (callsign, int(time.time()) - 1114880400, r) class TCPServerThread(threading.Thread): name = "[UNNAMEDSERVER]" def __init__(self, config, server, spec, handler): self.__server = server(spec, handler) self.__server.set_config(config) threading.Thread.__init__(self) self.setDaemon(True) def run(self): self.__server.serve_forever() print "%s Exiting" % self.name def stop(self): self.__server.shutdown() print "%s Shutdown" % self.name class POP3Exception(Exception): pass class POP3Exit(Exception): pass class POP3Handler(SocketServer.StreamRequestHandler): def _say(self, what, error=False): if error: c = "-ERR" else: c = "+OK" self.wfile.write(c + " %s\r\n" % what) print "[POP3] %s %s" % (c, what) def _handle_user(self, args): self._say("username %s accepted" % args) self._user = args return True def _handle_pass(self, args): self._say("password accepted") return True def get_messages(self, username): return [] def _handle_list(self, args): self._handle_stat(args) msgs = self.get_messages(self._user) index = 1 for msg in msgs: print "%i %i" % (index, len(str(msg))) self.wfile.write("%i %i \r\n" % (index, len(str(msg)))) index += 1 self.wfile.write(".\r\n") return True def _handle_stat(self, args): msgs = self.get_messages(self._user) size = 0 for i in msgs: size += len(str(i)) self._say("%i %i" % (len(msgs), size)) return False def _handle_retr(self, args): try: index = int(args) except Exception: utils.log_exception() raise POP3Exception("Invalid message number") m = self.get_message(index - 1) mstr = str(m) self._say("OK %i octets" % len(mstr)) self.wfile.write(mstr + "\r\n.\r\n") def _handle_top(self, args): msgno, lines = args.split(" ", 1) msgno = int(msgno) - 1 lines = int(lines) msg = self.get_message(msgno) self._say("top of message follows") self.wfile.write("\r\n".join(str(msg).split("\r\n")[:lines])) self.wfile.write("\r\n.\r\n") def _handle_dele(self, args): try: index = int(args) except Exception: utils.log_exception() raise POP3Exception("Invalid message number") self.del_message(index-1) self._say("Deleted") def _handle(self): dispatch = { "USER" : (("",), self._handle_user), "PASS" : (("USER",), self._handle_pass), "LIST" : (("PASS", "LIST", "STAT"), self._handle_list), "STAT" : (("PASS", "LIST", "STAT"), self._handle_stat), "RETR" : (("LIST", "STAT"), self._handle_retr), "TOP" : (("LIST", "STAT"), self._handle_top), "DELE" : (("LIST", "STAT"), self._handle_dele), } data = self.rfile.readline().strip() if not data: raise POP3Exception("Conversation error") try: cmd, args = data.split(" ", 1) except: cmd = data args = "" cmd = cmd.upper() print "[POP3] %s %s" % (cmd, args) if cmd == "QUIT": raise POP3Exit("Goodbye") if cmd not in dispatch.keys(): self._say("Unsupported command `%s'" % cmd, True) return states, handler = dispatch[cmd] if self.state not in states: raise POP3Exception("Can't get there from here") if handler(args): self.state = cmd def handle(self): self.state = "" self._say("D-RATS waiting") while True: try: self._handle() except POP3Exit, e: self._say(e) break except POP3Exception, e: self._say(e, True) break except Exception, e: utils.log_exception() self._say("Internal error", True) break class DRATS_POP3Handler(POP3Handler): def __init__(self, config, *args): self.__config = config print "DRATS handler" self.__msgcache = [] POP3Handler.__init__(self, *args) def get_messages(self, user): if self.__msgcache: return self.__msgcache if user.upper() == self.__config.get("user", "callsign"): d = os.path.join(self.__config.form_store_dir(), "Inbox") allmsg = True else: d = os.path.join(self.__config.form_store_dir(), "Outbox") allmsg = False files = glob(os.path.join(d, "*.xml")) for f in files: msg = msgrouting.form_to_email(self.__config, f) if not allmsg and msg["To"] != user.upper(): continue name, addr = email.utils.parseaddr(msg["From"]) if addr == "DO_NOT_REPLY@d-rats.com": addr = "%s@d-rats.com" % name.upper() msg.replace_header("From", addr) del msg["Reply-To"] name, addr = email.utils.parseaddr(msg["To"]) if not name and "@" not in addr: msg.replace_header("To", "%s@d-rats.com" % addr.upper()) msg["X-DRATS-Source"] = f self.__msgcache.append(msg) return self.__msgcache def get_message(self, index): return self.__msgcache[index] def del_message(self, index): msg = self.__msgcache[index] self.__msgcache[index] = None fn = msg["X-DRATS-Source"] if os.path.exists(fn): msgrouting.move_to_folder(self.__config, fn, "Trash") else: raise POP3Exception("Already deleted") class DRATS_POP3Server(SocketServer.TCPServer): allow_reuse_address = True def finish_request(self, request, client_address): self.RequestHandlerClass(self.__config, request, client_address, self) def set_config(self, config): self.__config = config class DRATS_POP3ServerThread(TCPServerThread): name = "[POP3]" def __init__(self, config): port = config.getint("settings", "msg_pop3_port") print "[POP3] Starting server on port %i" % port TCPServerThread.__init__(self, config, DRATS_POP3Server, ("0.0.0.0", port), DRATS_POP3Handler) self.setDaemon(True) class DRATS_SMTPServer(smtpd.SMTPServer): def __init__(self, config): self.__config = config port = config.getint("settings", "msg_smtp_port") smtpd.SMTPServer.__init__(self, ("0.0.0.0", port), None) def process_message(self, peer, mailfrom, rcpttos, data): msg = email.message_from_string(data) if "@" in mailfrom: sender, foo = mailfrom.split("@", 1) else: sender = mailfrom sender = sender.upper() if not re.match("[A-Z0-9]+", sender): raise Exception("Sender must be alphanumeric string") recip = rcpttos[0] if recip.lower().endswith("@d-rats.com"): recip, host = recip.upper().split("@", 1) print "Sender is %s" % sender print "Recip is %s" % recip mid = mkmsgid(self.__config.get("user", "callsign")) ffn = os.path.join(self.__config.form_store_dir(), "Outbox", "%s.xml" % mid) print "Storing mail at %s" % ffn form = emailgw.create_form_from_mail(self.__config, msg, ffn) form.set_path_src(sender) form.set_path_dst(recip) form.save_to(ffn) msgrouting.msg_unlock(ffn) class DRATS_SMTPServerThread(threading.Thread): def __init__(self, config): threading.Thread.__init__(self) self.setDaemon(True) self.__config = config self.__server = None def run(self): print "[SMTP] Starting server" self.__server = DRATS_SMTPServer(self.__config) asyncore.loop(timeout=1) print "[SMTP] Stopped" def stop(self): if self.__server: self.__server.close() self.join() if __name__ == "__main__": s = DRATS_POP3Server(("localhost", 9090), DRATS_POP3Handler) s.set_config(None) s.serve_forever() d-rats-0.3.3/d_rats/mainapp.py000077500000000000000000001141251160617671700162050ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import platform import os debug_path = platform.get_platform().config_file("debug.log") if sys.platform == "win32" or not os.isatty(0): sys.stdout = file(debug_path, "w", 0) sys.stderr = sys.stdout print "Enabled debug log" else: try: os.unlink(debug_path) except OSError: pass import gettext gettext.install("D-RATS") import time import re from threading import Thread, Lock from select import select import socket from commands import getstatusoutput import glob import shutil import datetime import serial import gtk import gobject import mainwindow import config import gps import mapdisplay import map_sources import comm import sessionmgr import session_coordinator import emailgw import formgui import station_status import pluginsrv import msgrouting import wl2k import inputdialog import version import agw import mailsrv from ui import main_events from utils import hexprint,filter_to_ascii,NetFile,log_exception,run_gtk_locked from utils import init_icon_maps from sessions import rpc, chat, sniff init_icon_maps() LOGTF = "%m-%d-%Y_%H:%M:%S" MAINAPP = None gobject.threads_init() def ping_file(filename): try: f = NetFile(filename, "r") except IOError, e: raise Exception("Unable to open file %s: %s" % (filename, e)) return None data = f.read() f.close() return data def ping_exec(command): s, o = getstatusoutput(command) if s: raise Exception("Failed to run command: %s" % command) return None return o class CallList(object): def __init__(self): self.clear() def clear(self): self.data = {} def set_call_pos(self, call, pos): (t, _) = self.data.get(call, (0, None)) self.data[call] = (t, pos) def set_call_time(self, call, ts=None): if ts is None: ts = time.time() (foo, p) = self.data.get(call, (0, None)) self.data[call] = (ts, p) def get_call_pos(self, call): (foo, p) = self.data.get(call, (0, None)) return p def get_call_time(self, call): (t, foo) = self.data.get(call, (0, None)) return t def list(self): return self.data.keys() def is_known(self, call): return self.data.has_key(call) def remove(self, call): try: del self.data[call] except: pass class MainApp(object): def setup_autoid(self): idtext = "(ID)" def stop_comms(self, portid): if self.sm.has_key(portid): sm, sc = self.sm[portid] sm.shutdown(True) sc.shutdown() del self.sm[portid] portspec, pipe = self.__pipes[portid] del self.__pipes[portid] self.__unused_pipes[portspec] = pipe return True else: return False def _make_socket_listeners(self, sc): forwards = self.config.options("tcp_out") for forward in forwards: try: sport, dport, station = \ self.config.get("tcp_out", forward).split(",") sport = int(sport) dport = int(dport) except Exception, e: print "Failed to parse TCP forward config %s: %s" % (forward, e) return try: sc.create_socket_listener(sport, dport, station) print "Started socket listener %i:%i@%s" % \ (sport, dport, station) except Exception, e: print "Failed to start socket listener %i:%i@%s: %s" % \ (sport, dport, station, e) def start_comms(self, portid): spec = self.config.get("ports", portid) try: enb, port, rate, dosniff, raw, name = spec.split(",") enb = (enb == "True") dosniff = (dosniff == "True") raw = (raw == "True") except Exception, e: print "Failed to parse portspec %s:" % spec log_exception() return if not enb: if self.sm.has_key(name): del self.sm[name] return print "Starting port %s (%s)" % (portid, name) call = self.config.get("user", "callsign") if self.__unused_pipes.has_key(port): path = self.__unused_pipes[port] del self.__unused_pipes[port] print "Re-using path %s for port %s" % (path, port) elif port.startswith("tnc-ax25:"): print port tnc, _port, tncport, path = port.split(":") path = path.replace(";", ",") _port = "%s:%s" % (_port, tncport) path = comm.TNCAX25DataPath((_port, int(rate), call, path)) elif port.startswith("tnc:"): _port = port.replace("tnc:", "") path = comm.TNCDataPath((_port, int(rate))) elif port.startswith("dongle:"): path = comm.SocketDataPath(("127.0.0.1", 20003, call, None)) elif port.startswith("agwpe:"): path = comm.AGWDataPath(port, 0.5) print "Opening AGW: %s" % path elif ":" in port: try: (mode, host, sport) = port.split(":") except ValueError: event = main_events.Event(None, _("Failed to connect to") + \ " %s: " % port + \ _("Invalid port string")) self.mainwindow.tabs["event"].event(event) return False path = comm.SocketDataPath((host, int(sport), call, rate)) else: path = comm.SerialDataPath((port, int(rate))) if self.__pipes.has_key(name): raise Exception("Port %s already started!" % name) self.__pipes[name] = (port, path) def transport_msg(msg): _port = name event = main_events.Event(None, "%s: %s" % (_port, msg)) gobject.idle_add(self.mainwindow.tabs["event"].event, event) transport_args = { "compat" : raw, "warmup_length" : self.config.getint("settings", "warmup_length"), "warmup_timeout" : self.config.getint("settings", "warmup_timeout"), "force_delay" : self.config.getint("settings", "force_delay"), "msg_fn" : transport_msg, } if not self.sm.has_key(name): sm = sessionmgr.SessionManager(path, call, **transport_args) chat_session = sm.start_session("chat", dest="CQCQCQ", cls=chat.ChatSession) self.__connect_object(chat_session, name) rpcactions = rpc.RPCActionSet(self.config, name) self.__connect_object(rpcactions) rpc_session = sm.start_session("rpc", dest="CQCQCQ", cls=rpc.RPCSession, rpcactions=rpcactions) def sniff_event(ss, src, dst, msg, port): if dosniff: event = main_events.Event(None, "Sniffer: %s" % msg) self.mainwindow.tabs["event"].event(event) self.mainwindow.tabs["stations"].saw_station(src, port) ss = sm.start_session("Sniffer", dest="CQCQCQ", cls=sniff.SniffSession) sm.set_sniffer_session(ss._id) ss.connect("incoming_frame", sniff_event, name) sc = session_coordinator.SessionCoordinator(self.config, sm) self.__connect_object(sc, name) sm.register_session_cb(sc.session_cb, None) self._make_socket_listeners(sc) self.sm[name] = sm, sc pingdata = self.config.get("settings", "ping_info") if pingdata.startswith("!"): def pingfn(): return ping_exec(pingdata[1:]) elif pingdata.startswith(">"): def pingfn(): return ping_file(pingdata[1:]) elif pingdata: def pingfn(): return pingdata else: pingfn = None chat_session.set_ping_function(pingfn) else: sm, sc = self.sm[name] sm.set_comm(path, **transport_args) sm.set_call(call) return True def chat_session(self, portname): return self.sm[portname][0].get_session(lid=1) def rpc_session(self, portname): return self.sm[portname][0].get_session(lid=2) def sc(self, portname): return self.sm[portname][1] def _refresh_comms(self): delay = False for portid in self.sm.keys(): print "Stopping %s" % portid if self.stop_comms(portid): if sys.platform == "win32": # Wait for windows to let go of the serial port delay = True if delay: time.sleep(0.25) for portid in self.config.options("ports"): print "Starting %s" % portid self.start_comms(portid) for spec, path in self.__unused_pipes.items(): print "Path %s for port %s no longer needed" % (path, spec) path.disconnect() self.__unused_pipes = {} def _static_gps(self): lat = 0.0 lon = 0.0 alt = 0.0 try: lat = self.config.get("user", "latitude") lon = self.config.get("user", "longitude") alt = self.config.get("user", "altitude") except Exception, e: import traceback traceback.print_exc(file=sys.stdout) print "Invalid static position: %s" % e print "Static position: %s,%s" % (lat,lon) return gps.StaticGPSSource(lat, lon, alt) def _refresh_gps(self): port = self.config.get("settings", "gpsport") rate = self.config.getint("settings", "gpsportspeed") enab = self.config.getboolean("settings", "gpsenabled") print "GPS: %s on %s@%i" % (enab, port, rate) if enab: if self.gps: self.gps.stop() if port.startswith("net:"): self.gps = gps.NetworkGPSSource(port) else: self.gps = gps.GPSSource(port, rate) self.gps.start() else: if self.gps: self.gps.stop() self.gps = self._static_gps() def _refresh_mail_threads(self): for k, v in self.mail_threads.items(): v.stop() del self.mail_threads[k] accts = self.config.options("incoming_email") for acct in accts: data = self.config.get("incoming_email", acct) if data.split(",")[-1] != "True": continue try: t = emailgw.PeriodicAccountMailThread(self.config, acct) except Exception: log_exception() continue self.__connect_object(t) t.start() self.mail_threads[acct] = t try: if self.config.getboolean("settings", "msg_smtp_server"): smtpsrv = mailsrv.DRATS_SMTPServerThread(self.config) smtpsrv.start() self.mail_threads["SMTPSRV"] = smtpsrv except Exception, e: print "Unable to start SMTP server: %s" % e log_exception() try: if self.config.getboolean("settings", "msg_pop3_server"): pop3srv = mailsrv.DRATS_POP3ServerThread(self.config) pop3srv.start() self.mail_threads["POP3SRV"] = pop3srv except Exception, e: print "Unable to start POP3 server: %s" % e log_exception() def _refresh_lang(self): locales = { "English" : "en", "German" : "de", "Italiano" : "it", "Dutch" : "nl", } locale = locales.get(self.config.get("prefs", "language"), "English") print "Loading locale `%s'" % locale localedir = os.path.join(platform.get_platform().source_dir(), "locale") print "Locale dir is: %s" % localedir if not os.environ.has_key("LANGUAGE"): os.environ["LANGUAGE"] = locale try: lang = gettext.translation("D-RATS", localedir=localedir, languages=[locale]) lang.install() gtk.glade.bindtextdomain("D-RATS", localedir) gtk.glade.textdomain("D-RATS") except LookupError: print "Unable to load language `%s'" % locale gettext.install("D-RATS") except IOError, e: print "Unable to load translation for %s: %s" % (locale, e) gettext.install("D-RATS") def _load_map_overlays(self): self.stations_overlay = None self.map.clear_map_sources() source_types = [map_sources.MapFileSource, map_sources.MapUSGSRiverSource, map_sources.MapNBDCBuoySource] for stype in source_types: try: sources = stype.enumerate(self.config) except Exception, e: import utils utils.log_exception() print "Failed to load source type %s" % stype continue for sname in sources: try: source = stype.open_source_by_name(self.config, sname) self.map.add_map_source(source) except Exception, e: log_exception() print "Failed to load map source %s: %s" % \ (source.get_name(), e) if sname == _("Stations"): self.stations_overlay = source if not self.stations_overlay: fn = os.path.join(self.config.platform.config_dir(), "static_locations", _("Stations") + ".csv") try: os.makedirs(os.path.dirname(fn)) except: pass file(fn, "w").close() self.stations_overlay = map_sources.MapFileSource(_("Stations"), "Static Overlay", fn) def refresh_config(self): print "Refreshing config..." call = self.config.get("user", "callsign") gps.set_units(self.config.get("user", "units")) mapdisplay.set_base_dir(self.config.get("settings", "mapdir")) mapdisplay.set_connected(self.config.getboolean("state", "connected_inet")) mapdisplay.set_tile_lifetime(self.config.getint("settings", "map_tile_ttl") * 3600) proxy = self.config.get("settings", "http_proxy") or None mapdisplay.set_proxy(proxy) self._refresh_comms() self._refresh_gps() self._refresh_mail_threads() def _refresh_location(self): fix = self.get_position() if not self.__map_point: self.__map_point = map_sources.MapStation(fix.station, fix.latitude, fix.longitude, fix.altitude, fix.comment) else: self.__map_point.set_latitude(fix.latitude) self.__map_point.set_longitude(fix.longitude) self.__map_point.set_altitude(fix.altitude) self.__map_point.set_comment(fix.comment) self.__map_point.set_name(fix.station) try: comment = self.config.get("settings", "default_gps_comment") fix.APRSIcon = gps.dprs_to_aprs(comment); except Exception, e: log_exception() fix.APRSIcon = "\?" self.__map_point.set_icon_from_aprs_sym(fix.APRSIcon) self.stations_overlay.add_point(self.__map_point) self.map.update_gps_status(self.gps.status_string()) return True def __chat(self, src, dst, data, incoming, port): if self.plugsrv: self.plugsrv.incoming_chat_message(src, dst, data) if src != "CQCQCQ": self.seen_callsigns.set_call_time(src, time.time()) kwargs = {} if dst != "CQCQCQ": to = " -> %s:" % dst kwargs["priv_src"] = src else: to = ":" if src == "CQCQCQ": color = "brokencolor" elif incoming: color = "incomingcolor" else: color = "outgoingcolor" if port: portstr = "[%s] " % port else: portstr = "" line = "%s%s%s %s" % (portstr, src, to, data) @run_gtk_locked def do_incoming(): self.mainwindow.tabs["chat"].display_line(line, incoming, color, **kwargs) gobject.idle_add(do_incoming) # ---------- STANDARD SIGNAL HANDLERS -------------------- def __status(self, object, status): self.mainwindow.set_status(status) def __user_stop_session(self, object, sid, port, force=False): print "User did stop session %i (force=%s)" % (sid, force) try: sm, sc = self.sm[port] session = sm.sessions[sid] session.close(force) except Exception, e: print "Session `%i' not found: %s" % (sid, e) def __user_cancel_session(self, object, sid, port): self.__user_stop_session(object, sid, port, True) def __user_send_form(self, object, station, port, fname, sname): self.sc(port).send_form(station, fname, sname) def __user_send_file(self, object, station, port, fname, sname): self.sc(port).send_file(station, fname, sname) def __user_send_chat(self, object, station, port, msg, raw): if raw: self.chat_session(port).write_raw(msg) else: self.chat_session(port).write(msg, station) def __incoming_chat_message(self, object, src, dst, data, port=None): if dst not in ["CQCQCQ", self.config.get("user", "callsign")]: # This is not destined for us return self.__chat(src, dst, data, True, port) def __outgoing_chat_message(self, object, src, dst, data, port=None): self.__chat(src, dst, data, False, port) def __get_station_list(self, object): stations = {} for port, (sm, sc) in self.sm.items(): stations[port] = [] station_list = self.mainwindow.tabs["stations"].get_stations() for station in station_list: if station.get_port() not in stations.keys(): print "Station %s has unknown port %s" % (station, station.get_port()) else: stations[station.get_port()].append(station) return stations def __get_message_list(self, object, station): return self.mainwindow.tabs["messages"].get_shared_messages(station) def __submit_rpc_job(self, object, job, port): self.rpc_session(port).submit(job) def __event(self, object, event): self.mainwindow.tabs["event"].event(event) def __config_changed(self, object): self.refresh_config() def __show_map_station(self, object, station): print "Showing map" self.map.show() def __ping_station(self, object, station, port): self.chat_session(port).ping_station(station) def __ping_station_echo(self, object, station, port, data, callback, cb_data): self.chat_session(port).ping_echo_station(station, data, callback, cb_data) def __ping_request(self, object, src, dst, data, port): msg = "%s pinged %s [%s]" % (src, dst, port) if data: msg += " (%s)" % data event = main_events.PingEvent(None, msg) self.mainwindow.tabs["event"].event(event) def __ping_response(self, object, src, dst, data, port): msg = "%s replied to ping from %s with: %s [%s]" % (src, dst, data, port) event = main_events.PingEvent(None, msg) self.mainwindow.tabs["event"].event(event) def __incoming_gps_fix(self, object, fix, port): ts = self.mainwindow.tabs["event"].last_event_time(fix.station) if (time.time() - ts) > 300: self.mainwindow.tabs["event"].finalize_last(fix.station) fix.set_relative_to_current(self.get_position()) event = main_events.PosReportEvent(fix.station, str(fix)) self.mainwindow.tabs["event"].event(event) self.mainwindow.tabs["stations"].saw_station(fix.station, port) def source_for_station(station): s = self.map.get_map_source(station) if s: return s try: print "Creating a map source for %s" % station s = map_sources.MapFileSource.open_source_by_name(self.config, station, True) except Exception, e: # Unable to create or add so use "Stations" overlay return self.stations_overlay self.map.add_map_source(s) return s if self.config.getboolean("settings", "timestamp_positions"): source = source_for_station(fix.station) fix.station = "%s.%s" % (fix.station, time.strftime("%Y%m%d%H%M%S")) else: source = self.stations_overlay point = map_sources.MapStation(fix.station, fix.latitude, fix.longitude, fix.altitude, fix.comment) point.set_icon_from_aprs_sym(fix.APRSIcon) source.add_point(point) source.save() def __station_status(self, object, sta, stat, msg, port): self.mainwindow.tabs["stations"].saw_station(sta, port, stat, msg) status = station_status.get_status_msgs()[stat] event = main_events.Event(None, "%s %s %s %s: %s" % (_("Station"), sta, _("is now"), status, msg)) self.mainwindow.tabs["event"].event(event) def __get_current_status(self, object, port): return self.mainwindow.tabs["stations"].get_status() def __get_current_position(self, object, station): if station is None: return self.get_position() else: sources = self.map.get_map_sources() for source in sources: if source.get_name() == _("Stations"): for point in source.get_points(): if point.get_name() == station: fix = gps.GPSPosition(point.get_latitude(), point.get_longitude()) return fix break raise Exception("Station not found") def __session_started(self, object, id, msg, port): # Don't register Chat, RPC, Sniff if id and id <= 3: return elif id == 0: msg = "Port connected" print "[SESSION %i]: %s" % (id, msg) event = main_events.SessionEvent(id, port, msg) self.mainwindow.tabs["event"].event(event) return event def __session_status_update(self, object, id, msg, port): self.__session_started(object, id, msg, port) def __session_ended(self, object, id, msg, restart_info, port): # Don't register Control, Chat, RPC, Sniff if id <= 4: return event = self.__session_started(object, id, msg, port) event.set_restart_info(restart_info) event.set_as_final() fn = None if restart_info: fn = restart_info[1] self.msgrouter.form_xfer_done(fn, port, True) def __form_received(self, object, id, fn, port=None): if port: id = "%s_%s" % (id, port) print "[NEWFORM %s]: %s" % (id, fn) f = formgui.FormFile(fn) msg = '%s "%s" %s %s' % (_("Message"), f.get_subject_string(), _("received from"), f.get_sender_string()) myc = self.config.get("user", "callsign") dst = f.get_path_dst() src = f.get_path_src() pth = f.get_path() fwd_on = self.config.getboolean("settings", "msg_forward"); is_dst = msgrouting.is_sendable_dest(myc, dst) nextst = msgrouting.gratuitous_next_hop(dst, pth) or dst bounce = "@" in src and "@" in dst isseen = myc in f.get_path()[:-1] print "Decision: " + \ "fwd:%s " % fwd_on + \ "sendable:%s " % is_dst + \ "next:%s " % nextst + \ "bounce:%s " % bounce + \ "seen:%s " % isseen if fwd_on and is_dst and not bounce and not isseen: msg += " (%s %s)" % (_("forwarding to"), nextst) msgrouting.move_to_outgoing(self.config, fn) refresh_folder = "Outbox" else: refresh_folder = "Inbox" msgrouting.msg_unlock(fn) self.mainwindow.tabs["messages"].refresh_if_folder(refresh_folder) event = main_events.FormEvent(id, msg) event.set_as_final() self.mainwindow.tabs["event"].event(event) def __file_received(self, object, id, fn, port=None): if port: id = "%s_%s" % (id, port) _fn = os.path.basename(fn) msg = '%s "%s" %s' % (_("File"), _fn, _("Received")) event = main_events.FileEvent(id, msg) event.set_as_final() self.mainwindow.tabs["files"].refresh_local() self.mainwindow.tabs["event"].event(event) def __form_sent(self, object, id, fn, port=None): self.msgrouter.form_xfer_done(fn, port, False) if port: id = "%s_%s" % (id, port) print "[FORMSENT %s]: %s" % (id, fn) event = main_events.FormEvent(id, _("Message Sent")) event.set_as_final() self.mainwindow.tabs["messages"].message_sent(fn) self.mainwindow.tabs["event"].event(event) def __file_sent(self, object, id, fn, port=None): if port: id = "%s_%s" % (id, port) print "[FILESENT %s]: %s" % (id, fn) _fn = os.path.basename(fn) msg = '%s "%s" %s' % (_("File"), _fn, _("Sent")) event = main_events.FileEvent(id, msg) event.set_as_final() self.mainwindow.tabs["files"].file_sent(fn) self.mainwindow.tabs["event"].event(event) def __get_chat_port(self, object): return self.mainwindow.tabs["chat"].get_selected_port() def __trigger_msg_router(self, object, account): if not account: self.msgrouter.trigger() elif account == "@WL2K": call = self.config.get("user", "callsign") mt = wl2k.wl2k_auto_thread(self, call) self.__connect_object(mt) mt.start() elif account in self.mail_threads.keys(): self.mail_threads[account].trigger() else: mt = emailgw.AccountMailThread(self.config, account) mt.start() def __register_object(self, parent, object): self.__connect_object(object) # ------------ END SIGNAL HANDLERS ---------------- def __connect_object(self, object, *args): for signal in object._signals.keys(): handler = self.handlers.get(signal, None) if handler is None: pass #raise Exception("Object signal `%s' of object %s not known" % \ # (signal, object)) elif self.handlers[signal]: try: object.connect(signal, handler, *args) except Exception: print "Failed to attach signal %s" % signal raise def _announce_self(self): print ("-" * 75) print "D-RATS v%s starting at %s" % (version.DRATS_VERSION, time.asctime()) print platform.get_platform() print ("-" * 75) def __init__(self, **args): self.handlers = { "status" : self.__status, "user-stop-session" : self.__user_stop_session, "user-cancel-session" : self.__user_cancel_session, "user-send-form" : self.__user_send_form, "user-send-file" : self.__user_send_file, "rpc-send-form" : self.__user_send_form, "rpc-send-file" : self.__user_send_file, "user-send-chat" : self.__user_send_chat, "incoming-chat-message" : self.__incoming_chat_message, "outgoing-chat-message" : self.__outgoing_chat_message, "get-station-list" : self.__get_station_list, "get-message-list" : self.__get_message_list, "submit-rpc-job" : self.__submit_rpc_job, "event" : self.__event, "notice" : False, "config-changed" : self.__config_changed, "show-map-station" : self.__show_map_station, "ping-station" : self.__ping_station, "ping-station-echo" : self.__ping_station_echo, "ping-request" : self.__ping_request, "ping-response" : self.__ping_response, "incoming-gps-fix" : self.__incoming_gps_fix, "station-status" : self.__station_status, "get-current-status" : self.__get_current_status, "get-current-position" : self.__get_current_position, "session-status-update" : self.__session_status_update, "session-started" : self.__session_started, "session-ended" : self.__session_ended, "file-received" : self.__file_received, "form-received" : self.__form_received, "file-sent" : self.__file_sent, "form-sent" : self.__form_sent, "get-chat-port" : self.__get_chat_port, "trigger-msg-router" : self.__trigger_msg_router, "register-object" : self.__register_object, } global MAINAPP MAINAPP = self self.comm = None self.sm = {} self.seen_callsigns = CallList() self.position = None self.mail_threads = {} self.__unused_pipes = {} self.__pipes = {} self.pop3srv = None self.config = config.DratsConfig(self) self._refresh_lang() self._announce_self() message = _("Since this is your first time running D-RATS, " + "you will be taken directly to the configuration " + "dialog. At a minimum, put your callsign in the " + "box and click 'Save'. You will be connected to " + "the ratflector initially for testing.") while self.config.get("user", "callsign") == "": d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK) d.set_markup(message) d.run() d.destroy() if not self.config.show(): raise Exception("User canceled configuration") message = _("You must enter a callsign to continue") self.gps = self._static_gps() self.map = mapdisplay.MapWindow(self.config) self.map.set_title("D-RATS Map Window") self.map.connect("reload-sources", lambda m: self._load_map_overlays()) self.__connect_object(self.map) pos = self.get_position() self.map.set_center(pos.latitude, pos.longitude) self.map.set_zoom(14) self.__map_point = None self.mainwindow = mainwindow.MainWindow(self.config) self.__connect_object(self.mainwindow) for tab in self.mainwindow.tabs.values(): self.__connect_object(tab) self.refresh_config() self._load_map_overlays() if self.config.getboolean("prefs", "dosignon") and self.chat_session: msg = self.config.get("prefs", "signon") status = station_status.STATUS_ONLINE for port in self.sm.keys(): self.chat_session(port).advertise_status(status, msg) gobject.timeout_add(3000, self._refresh_location) def get_position(self): p = self.gps.get_position() p.set_station(self.config.get("user", "callsign")) try: p.set_station(self.config.get("user", "callsign"), self.config.get("settings", "default_gps_comment")) except Exception: pass return p def load_static_routes(self): routes = self.config.platform.config_file("routes.txt") if not os.path.exists(routes): return f = file(routes) lines = f.readlines() lno = 0 for line in lines: lno += 1 if not line.strip() or line.startswith("#"): continue try: routeto, station, port = line.split() except Exception: print "Line %i of %s not valid" % (lno, routes) continue self.mainwindow.tabs["stations"].saw_station(station.upper(), port) if self.sm.has_key(port): sm, sc = self.sm[port] sm.manual_heard_station(station) def clear_all_msg_locks(self): path = os.path.join(self.config.platform.config_dir(), "messages", "*", ".lock*") for lock in glob.glob(path): print "Removing stale message lock %s" % lock os.remove(lock) def main(self): # Copy default forms before we start distdir = platform.get_platform().source_dir() userdir = self.config.form_source_dir() dist_forms = glob.glob(os.path.join(distdir, "forms", "*.x?l")) for form in dist_forms: fname = os.path.basename(form) user_fname = os.path.join(userdir, fname) try: needupd = \ (os.path.getmtime(form) > os.path.getmtime(user_fname)) except Exception: needupd = True if not os.path.exists(user_fname) or needupd: print "Installing dist form %s -> %s" % (fname, user_fname) try: shutil.copyfile(form, user_fname) except Exception, e: print "FAILED: %s" % e self.clear_all_msg_locks() if len(self.config.options("ports")) == 0 and \ self.config.has_option("settings", "port"): print "Migrating single-port config to multi-port" port = self.config.get("settings", "port") rate = self.config.get("settings", "rate") snif = self.config.getboolean("settings", "sniff_packets") comp = self.config.getboolean("settings", "compatmode") self.config.set("ports", "port_0", "%s,%s,%s,%s,%s,%s" % (True, port, rate, snif, comp, "DEFAULT")) for i in ["port", "rate", "sniff_packets", "compatmode"]: self.config.remove_option("settings", i) try: self.plugsrv = pluginsrv.DRatsPluginServer() self.__connect_object(self.plugsrv.get_proxy()) self.plugsrv.serve_background() except Exception, e: print "Unable to start plugin server: %s" % e self.plugsrv = None self.load_static_routes() try: self.msgrouter = msgrouting.MessageRouter(self.config) self.__connect_object(self.msgrouter) self.msgrouter.start() except Exception, e: log_exception() self.msgrouter = None try: gtk.main() except KeyboardInterrupt: pass except Exception, e: print "Got exception on close: %s" % e print "Saving config..." self.config.save() if self.config.getboolean("prefs", "dosignoff") and self.sm: msg = self.config.get("prefs", "signoff") status = station_status.STATUS_OFFLINE for port in self.sm.keys(): self.chat_session(port).advertise_status(status, msg) time.sleep(0.5) # HACK def get_mainapp(): return MAINAPP d-rats-0.3.3/d_rats/mainwindow.py000066400000000000000000000265351160617671700167400ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys if __name__ == "__main__": sys.path.insert(0, ".") from d_rats import mainapp from d_rats import platform import gettext lang = gettext.translation("D-RATS", localedir="./locale", languages=["en"]) lang.install() print sys.path import os import time import libxml2 import gtk import gtk.glade import gobject import subprocess from d_rats.ui.main_messages import MessagesTab from d_rats.ui.main_chat import ChatTab from d_rats.ui.main_events import EventTab from d_rats.ui.main_files import FilesTab from d_rats.ui.main_stations import StationsList from d_rats.ui.main_common import MainWindowElement, prompt_for_station, \ ask_for_confirmation from d_rats.version import DRATS_VERSION from d_rats import formbuilder from d_rats import signals class MainWindow(MainWindowElement): __gsignals__ = { "config-changed" : signals.CONFIG_CHANGED, "show-map-station" : signals.SHOW_MAP_STATION, "ping-station" : signals.PING_STATION, "get-station-list" : signals.GET_STATION_LIST, "user-send-chat" : signals.USER_SEND_CHAT, "get-chat-port" : signals.GET_CHAT_PORT, } _signals = __gsignals__ def _delete(self, window, event): if self._config.getboolean("prefs", "confirm_exit"): if not ask_for_confirmation("Really exit D-RATS?", window): return True window.set_default_size(*window.get_size()) def _destroy(self, window): w, h = window.get_size() maximized = window.maximize_initially self._config.set("state", "main_maximized", maximized) if not maximized: self._config.set("state", "main_size_x", w) self._config.set("state", "main_size_y", h) gtk.main_quit() def _connect_menu_items(self, window): def do_save_and_quit(but): window.set_default_size(*window.get_size()) window.destroy() def do_about(but): d = gtk.AboutDialog() d.set_transient_for(self._wtree.get_widget("mainwindow")) verinfo = "GTK %s\nPyGTK %s\nLibXML using %.1f KB\n" % ( \ ".".join([str(x) for x in gtk.gtk_version]), ".".join([str(x) for x in gtk.pygtk_version]), libxml2.memoryUsed() / 1024.0) d.set_name("D-RATS") d.set_version(DRATS_VERSION) d.set_copyright("Copyright 2010 Dan Smith (KK7DS)") d.set_website("http://www.d-rats.com") d.set_authors(("Dan Smith ",)) d.set_comments(verinfo) d.set_translator_credits("Italian: Leo, IZ5FSA") d.run() d.destroy() def do_debug(but): path = self._config.platform.config_file("debug.log") if os.path.exists(path): self._config.platform.open_text_file(path) else: d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=window) d.set_property("text", "Debug log not available") d.run() d.destroy() def do_prefs(but): saved = self._config.show(parent=window) if saved: self.emit("config-changed") for tabs in self.tabs.values(): tabs.reconfigure() def do_map(but): call = self._config.get("user", "callsign") self.emit("show-map-station", call) def do_message_templates(but): d = formbuilder.FormManagerGUI(self._config.form_source_dir()) def do_ping(but): station_list = self.emit("get-station-list") stations = [] for portlist in station_list.values(): stations += [str(x) for x in portlist] station, port = prompt_for_station(stations, self._config) if station: self.emit("ping-station", station, port) def do_conninet(but): self._config.set("state", "connected_inet", but.get_active()) def do_showpane(but, pane): self._config.set("state", "sidepane_visible", but.get_active()) if but.get_active(): pane.show() else: pane.hide() def do_dq(but): c = self._config wtree = gtk.glade.XML(c.ship_obj_fn("ui/mainwindow.glade"), "dquery_dialog", "D-RATS") dlg = wtree.get_widget("dquery_dialog") cmd = wtree.get_widget("dq_cmd") dlg.set_modal(True) dlg.set_transient_for(window) r = dlg.run() d = cmd.get_text() dlg.destroy() if r == gtk.RESPONSE_OK: port = self.emit("get-chat-port") self.emit("user-send-chat", "CQCQCQ", port, "?D*%s?" % d, True) def do_proxy(but): if sys.platform != "darwin": args = [] else: args = [sys.executable] if os.path.exists("./d-rats_repeater"): args.append("./d-rats_repeater") else: args.append("d-rats_repeater") print "Running proxy: %s" % str(args) p = subprocess.Popen(args) quit = self._wtree.get_widget("main_menu_quit") quit.connect("activate", do_save_and_quit) about = self._wtree.get_widget("main_menu_about") about.connect("activate", do_about) debug = self._wtree.get_widget("main_menu_debuglog") debug.connect("activate", do_debug) menu_prefs = self._wtree.get_widget("main_menu_prefs") menu_prefs.connect("activate", do_prefs) menu_map = self._wtree.get_widget("main_menu_map") img = gtk.Image() img.set_from_file("images/map.png") menu_map.set_image(img) menu_map.connect("activate", do_map) menu_templates = self._wtree.get_widget("main_menu_msgtemplates") menu_templates.connect("activate", do_message_templates) ping = self._wtree.get_widget("main_menu_ping") img = gtk.Image() img.set_from_file("images/event_ping.png") ping.set_image(img) ping.connect("activate", do_ping) conn = self._wtree.get_widget("main_menu_conninet") conn.set_active(self._config.getboolean("state", "connected_inet")) self._config.platform.set_connected(conn.get_active()) conn.connect("activate", do_conninet) sspw = self._wtree.get_widget("main_menu_showpane") pane = self._wtree.get_widget("main_stations_frame") sspw.set_active(self._config.getboolean("state", "sidepane_visible")) if not sspw.get_active(): pane.hide() sspw.connect("activate", do_showpane, pane) dq = self._wtree.get_widget("main_menu_dq") dq.connect("activate", do_dq) proxy = self._wtree.get_widget("main_menu_proxy") proxy.connect("activate", do_proxy) def _page_name(self, index=None): if index is None: index = self._tabs.get_current_page() tablabels = ["messages", "chat", "files", "event"] return tablabels[index] def _tab_switched(self, tabs, page, page_num): tab = self._page_name(page_num) self.tabs[self._current_tab].deselected() self._current_tab = tab self.tabs[self._current_tab].selected() def _maybe_blink(self, tab, key): blink = self._config.getboolean("prefs", "blink_%s" % key) if blink and not self.__window.is_active(): self.__window.set_urgency_hint(True) if key == "event": sounde = False else: sounde = self._config.getboolean("sounds", "%s_enabled" % key) soundf = self._config.get("sounds", key) if sounde: self._config.platform.play_sound(soundf) def _got_focus(self, window, event): self.__window.set_urgency_hint(False) def __init__(self, config): wtree = gtk.glade.XML(config.ship_obj_fn("ui/mainwindow.glade"), "mainwindow", "D-RATS") MainWindowElement.__init__(self, wtree, config, "") self.__window = self._wtree.get_widget("mainwindow") self._tabs = self._wtree.get_widget("main_tabs") self._tabs.connect("switch-page", self._tab_switched) self.tabs = {} self.__last_status = 0 self.tabs["chat"] = ChatTab(wtree, config) self.tabs["messages"] = MessagesTab(wtree, config) self.tabs["event"] = EventTab(wtree, config) self.tabs["files"] = FilesTab(wtree, config) self.tabs["stations"] = StationsList(wtree, config) for label, tab in self.tabs.items(): tab.connect("notice", self._maybe_blink, label) self._current_tab = "messages" ic = "incomingcolor" cpr = "Copyright 2010 Dan Smith (KK7DS)" self.tabs["chat"]._display_line("D-RATS v%s" % DRATS_VERSION, True, ic) self.tabs["chat"]._display_line(cpr, True, ic) self.tabs["chat"]._display_line("", True) self.__window.connect("destroy", self._destroy) self.__window.connect("delete_event", self._delete) self.__window.connect("focus-in-event", self._got_focus) self._connect_menu_items(self.__window) h = self._config.getint("state", "main_size_x") w = self._config.getint("state", "main_size_y") if self._config.getboolean("state", "main_maximized"): self.__window.maximize() self.__window.set_default_size(h, w) else: self.__window.resize(h, w) self.__window.show() gobject.timeout_add(3000, self.__update_status) def __update_status(self): if (time.time() - self.__last_status) > 30: sb = self._wtree.get_widget("statusbar") id = sb.get_context_id("default") sb.pop(id) return True def set_status(self, msg): sb = self._wtree.get_widget("statusbar") cb = self._wtree.get_widget("callbar") self.__last_status = time.time() id = sb.get_context_id("default") sb.pop(id) sb.push(id, msg) call = self._config.get("user", "callsign") self.__window.set_title("D-RATS: %s" % call) cb.pop(0) cb.push(0, call) if __name__ == "__main__": wtree = gtk.glade.XML("ui/mainwindow.glade", "mainwindow") from d_rats import config conf = config.DratsConfig(None) def test(chat, station, msg): print "%s->%s" % (station, msg) chat = ChatTab(wtree, conf) chat.connect("user-sent-message", test) msgs = MessagesTab(wtree, conf) gtk.main() d-rats-0.3.3/d_rats/map_source_editor.py000066400000000000000000000217021160617671700202560ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil from glob import glob import gtk import gobject import map_sources import miscwidgets import utils class EditorInitCancel(Exception): pass class MapSourcesEditor(object): def _add(self, button, typesel): t = typesel.get_active_text() try: et, st = SOURCE_TYPES[t] e = et(self.__config) except EditorInitCancel: return r = e.run() if r == gtk.RESPONSE_OK: e.save() self.__store.append((t, e.get_source().get_name(), e)) e.destroy() def _rem(self, button): (model, iter) = self.__view.get_selection().get_selected() e, = self.__store.get(iter, 2) e.delete() model.remove(iter) def _edit(self, button): (model, iter) = self.__view.get_selection().get_selected() e, = self.__store.get(iter, 2) e.run() e.save() e.destroy() def _setup_view(self): self.__store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT) self.__view.set_model(self.__store) c = gtk.TreeViewColumn(_("Type"), gtk.CellRendererText(), text=0) self.__view.append_column(c) c = gtk.TreeViewColumn(_("Name"), gtk.CellRendererText(), text=1) self.__view.append_column(c) def _setup_typesel(self, wtree): choice = miscwidgets.make_choice(SOURCE_TYPES.keys(), False, "Static") choice.show() box = wtree.get_widget("srcs_ctrlbox") box.pack_end(choice, 1, 1, 1) return choice def __init__(self, config): fn = config.ship_obj_fn("ui/mainwindow.glade") if not os.path.exists(fn): print fn raise Exception("Unable to load UI file") wtree = gtk.glade.XML(fn, "srcs_dialog", "D-RATS") self.__config = config self.__dialog = wtree.get_widget("srcs_dialog") self.__view = wtree.get_widget("srcs_view") addbtn = wtree.get_widget("srcs_add") editbtn = wtree.get_widget("srcs_edit") delbtn = wtree.get_widget("srcs_delete") self._setup_view() typesel = self._setup_typesel(wtree) addbtn.connect("clicked", self._add, typesel) editbtn.connect("clicked", self._edit) delbtn.connect("clicked", self._rem) for stype, (edclass, srcclass) in SOURCE_TYPES.items(): for key in srcclass.enumerate(self.__config): try: src = srcclass.open_source_by_name(self.__config, key) sed = edclass(self.__config, src) self.__store.append((stype, sed.get_source().get_name(), sed)) except Exception, e: utils.log_exception() print "Failed to open source %s:%s" % (stype, key) def run(self): return self.__dialog.run() def destroy(self): self.__dialog.destroy() class MapSourceEditor(object): def __init__(self, config, source): self._config = config self.__source = source fn = config.ship_obj_fn("ui/mainwindow.glade") if not os.path.exists(fn): print fn raise Exception("Unable to load UI file") self._wtree = gtk.glade.XML(fn, "src_dialog", "D-RATS") self.__dialog = self._wtree.get_widget("src_dialog") self._name = self._wtree.get_widget("src_name") self._name.set_text(source.get_name()) def get_source(self): return self.__source def get_name(self): return self._name.get_text() def name_editable(self, editable): self._name.set_sensitive(editable) def run(self): return self.__dialog.run() def destroy(self): self.__dialog.hide() def delete(self): pass def save(self): pass class StaticMapSourceEditor(MapSourceEditor): def __init__(self, config, source=None): if not source: fn = config.platform.gui_open_file() if not fn: raise EditorInitCancel() nfn = os.path.join(config.platform.config_file("static_locations"), os.path.basename(fn)) shutil.copy(fn, nfn) fn = nfn source = map_sources.MapFileSource(os.path.basename(nfn), "Static Source", nfn) MapSourceEditor.__init__(self, config, source) label = gtk.Label("Nothing to edit here") label.show() box = self._wtree.get_widget("src_vbox") box.pack_start(label, 1, 1, 1) self.name_editable(False) def delete(self): os.remove(self.get_source().get_filename()) def save(self): self.get_source().save() class RiverMapSourceEditor(MapSourceEditor): def __init__(self, config, source=None): if not source: source = map_sources.MapUSGSRiverSource("Rivers", "NWIS Rivers") name_editable = True else: name_editable = False MapSourceEditor.__init__(self, config, source) box = self._wtree.get_widget("src_vbox") hbox = gtk.HBox(False, 2) hbox.show() label = gtk.Label(_("Sites (comma separated)")) label.show() hbox.pack_start(label, 0, 0, 0) self.__sites = gtk.Entry() self.__sites.show() _sites = [str(x) for x in source.get_sites()] self.__sites.set_text(",".join(_sites)) hbox.pack_start(self.__sites, 1, 1, 1) box.pack_start(hbox, 1, 1, 1) self.name_editable(name_editable) def delete(self): id = self.get_source().packed_name() try: self._config.remove_option("rivers", id) self._config.remove_option("rivers", "%s.label" % id) except Exception, e: log_exception() print "Error deleting rivers/%s: %s" % (id, e) def save(self): if not self._config.has_section("rivers"): self._config.add_section("rivers") self.get_source().set_name(self.get_name()) id = self.get_source().packed_name() self._config.set("rivers", id, self.__sites.get_text()) self._config.set("rivers", "%s.label" % id, self.get_source().get_name()) class BuoyMapSourceEditor(MapSourceEditor): def __init__(self, config, source=None): if not source: source = map_sources.MapNBDCBuoySource("Buoys", "NBDC Rivers") name_editable = True else: name_editable = False MapSourceEditor.__init__(self, config, source) box = self._wtree.get_widget("src_vbox") hbox = gtk.HBox(False, 2) hbox.show() label = gtk.Label(_("Buoys (comma separated)")) label.show() hbox.pack_start(label, 0, 0, 0) self.__sites = gtk.Entry() self.__sites.show() _sites = [str(x) for x in source.get_buoys()] self.__sites.set_text(",".join(_sites)) hbox.pack_start(self.__sites, 1, 1, 1) box.pack_start(hbox, 1, 1, 1) self.name_editable(name_editable) def delete(self): id = self.get_source().packed_name() try: self._config.remove_option("buoys", id) self._config.remove_option("buoys", "%s.label" % id) except Exception, e: log_exception() print "Error deleting buoys/%s: %s" % (id, e) def save(self): if not self._config.has_section("buoys"): self._config.add_section("buoys") self.get_source().set_name(self.get_name()) id = self.get_source().packed_name() self._config.set("buoys", id, self.__sites.get_text()) self._config.set("buoys", "%s.label" % id, self.get_source().get_name()) SOURCE_TYPES = { "Static" : (StaticMapSourceEditor, map_sources.MapFileSource), "NWIS River" : (RiverMapSourceEditor, map_sources.MapUSGSRiverSource), "NBDC Buoy" : (BuoyMapSourceEditor, map_sources.MapNBDCBuoySource), } d-rats-0.3.3/d_rats/map_sources.py000066400000000000000000000406321160617671700170760ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time import threading import os from glob import glob import libxml2 import gobject import utils import platform class Callable: def __init__(self, target): self.__call__ = target class MapItem(object): pass class MapPoint(gobject.GObject): __gsignals__ = { "updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), } def _retr_hook(self, method, attribute): pass def __init__(self): gobject.GObject.__init__(self) self.__latitude = 0.0 self.__longitude = 0.0 self.__altitude = 0.0 self.__name = "" self.__comment = "" self.__icon = None self.__timestamp = time.time() self.__visible = True def dup(self): p = MapPoint() for i in ["latitude", "longitude", "altitude", "name", "comment", "icon", "timestamp", "visible"]: k = "_MapPoint__" + i p.__dict__[k] = self.__dict__[k] return p def __getattr__(self, name): _get, name = name.split("_", 1) self._retr_hook(_get, name) attrname = "_MapPoint__%s" % name #print self.__dict__.keys() if not hasattr(self, attrname): raise ValueError("No such attribute `%s'" % attrname) def get(): return self.__dict__[attrname] def set(val): self.__dict__[attrname] = val if _get == "get": return get elif _get == "set": return set else: pass def __repr__(self): msg = "%s@%.4f,%.4f" % (self.get_name(), self.get_latitude(), self.get_longitude()) return msg def __str__(self): return self.get_name() def __eq__(self, point): return self.get_name() == point.get_name() class MapStation(MapPoint): def __init__(self, call, lat, lon, alt=0.0, comment=""): MapPoint.__init__(self) self.set_latitude(lat) self.set_longitude(lon) self.set_altitude(alt) self.set_name(call) self.set_comment(comment) self._aprs_sym = "" # FIXME: Set icon from DPRS comment def set_icon_from_aprs_sym(self, symbol): self.set_icon(utils.get_icon(symbol)) self._aprs_sym = symbol def get_aprs_symbol(self): return self._aprs_sym def _xdoc_getnodeval(ctx, nodespec): items = ctx.xpathEval(nodespec) if len(items) == 0: raise Exception("No data for %s" % nodespec) if len(items) > 1: raise Exception("Too many nodes") return items[0].getContent() class MapPointThreaded(MapPoint): def __init__(self): MapPoint.__init__(self) self.__thread = None self.__ts = 0 def __start_thread(self): if self.__thread and self.__thread.isAlive(): print "Threaded Point: Still waiting on a thread" return self.__thread = threading.Thread(target=self.__thread_fn) self.__thread.setDaemon(True) self.__thread.start() def _retr_hook(self, method, attribute): if time.time() - self.__ts > 60: try: self.__ts = time.time() self.__start_thread() except Exception, e: print "Can't start: %s" % e def __thread_fn(self): self.do_update() gobject.idle_add(self.emit, "updated", "FOO") class MapUSGSRiver(MapPointThreaded): def do_update(self): print "[River %s] Doing update..." % self.__site if not self.__have_site: try: self.__parse_site() self.__have_site = True except Exception, e: utils.log_exception() print "[River %s] Failed to parse site: %s" % (self.__site, e) self.set_name("Invalid river %s" % self.__site) try: self.__parse_level() except Exception, e: utils.log_exception() print "[River %s] Failed to parse level: %s" % (self.__site, e) print "[River %s] Done with update" % self.__site def __parse_site(self): url = "http://waterdata.usgs.gov/nwis/inventory?search_site_no=%s&format=sitefile_output&sitefile_output_format=xml&column_name=agency_cd&column_name=site_no&column_name=station_nm&column_name=dec_lat_va&column_name=dec_long_va&column_name=alt_va" % self.__site p = platform.get_platform() try: fn, headers = p.retrieve_url(url) content = file(fn).read() except Exception, e: print "[NSGS] Failed to fetch info for %s: %s" % (self.__site, e) self.set_name("NSGS NWIS Site %s" % self.__site) return doc = libxml2.parseMemory(content, len(content)) ctx = doc.xpathNewContext() base = "/usgs_nwis/site/" self._basename = _xdoc_getnodeval(ctx, base + "station_nm") self.set_name(self._basename) self.set_latitude(float(_xdoc_getnodeval(ctx, base + "dec_lat_va"))) self.set_longitude(float(_xdoc_getnodeval(ctx, base + "dec_long_va"))) self.set_altitude(float(_xdoc_getnodeval(ctx, base + "alt_va"))) def __parse_level(self): url = "http://waterdata.usgs.gov/nwis/uv?format=rdb&period=1&site_no=%s" % self.__site p = platform.get_platform() try: fn, headers = p.retrieve_url(url) line = file(fn).readlines()[-1] except Exception, e: print "[NSGS] Failed to fetch info for site %s: %s" % (self.__site, e) self.set_comment("No data") self.set_timestamp(time.time()) return fields = line.split("\t") self._height_ft = float(fields[3]) self.set_comment("River height: %.1f ft" % self._height_ft) self.set_timestamp(time.time()) def __init__(self, site): MapPointThreaded.__init__(self) self.__site = site self.__have_site = False self.set_icon(utils.get_icon("/w")) class MapNBDCBuoy(MapPointThreaded): def do_update(self): p = platform.get_platform() try: fn, headers = p.retrieve_url(self.__url) content = file(fn).read() except Exception, e: print "[NBDC] Failed to fetch info for %i: %s" % (self.__buoy, e) self.set_name("NBDC %s" % self.__buoy) return try: doc = libxml2.parseMemory(content, len(content)) except Exception, e: print "[NBDC] Failed to parse document %s: %s" % (self.__url, e) self.set_name("NBDC Unknown Buoy %s" % self.__buoy) return ctx = doc.xpathNewContext() ctx.xpathRegisterNs("georss", "http://www.georss.org/georss") base = "/rss/channel/item/" self.set_name(_xdoc_getnodeval(ctx, base + "title")) try: s = _xdoc_getnodeval(ctx, base + "description") except Exception, e: print "[Buoy %s] Unable to get description: %s" % (self.__buoy, e) return for i in ["", "", "
", "°"]: s = s.replace("%s" % i, "") self.set_comment(s) self.set_timestamp(time.time()) try: slat, slon = _xdoc_getnodeval(ctx, base + "georss:point").split(" ", 1) except Exception, e: utils.log_exception() print "[Buoy %s]: Result has no georss:point" % self.__buoy return self.set_latitude(float(slat)) self.set_longitude(float(slon)) print "[Buoy %s] Done with update" % self.__buoy def __init__(self, buoy): MapPointThreaded.__init__(self) self.__buoy = buoy self.__url = "http://www.ndbc.noaa.gov/data/latest_obs/%s.rss" % buoy self.set_icon(utils.get_icon("\\N")) class MapSourceFailedToConnect(Exception): pass class MapSourcePointError(Exception): pass class MapSource(gobject.GObject): __gsignals__ = { "point-added" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), "point-deleted" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), "point-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), } def __init__(self, name, description, color="red"): gobject.GObject.__init__(self) self._name = name self._description = description self._points = {} self._color = color self._visible = True self._mutable = True def save(self): pass def add_point(self, point): had = self._points.has_key(point.get_name()) self._points[point.get_name()] = point if had: self.emit("point-updated", point) else: self.emit("point-added", point) def del_point(self, point): del self._points[point.get_name()] self.emit("point-deleted", point) def get_points(self): return self._points.values() def get_point_by_name(self, name): return self._points[name] def get_color(self): return self._color def get_name(self): return self._name def set_name(self, name): self._name = name def get_description(self): return self._description def get_visible(self): return self._visible def set_visible(self, visible): self._visible = visible def get_mutable(self): return self._mutable class MapFileSource(MapSource): def _enumerate(config): dirpath = os.path.join(config.platform.config_dir(), "static_locations") files = glob(os.path.join(dirpath, "*.*")) return [os.path.splitext(os.path.basename(f))[0] for f in files] enumerate = Callable(_enumerate) def _open_source_by_name(config, name, create=False): dirpath = os.path.join(config.platform.config_dir(), "static_locations") path = os.path.join(dirpath, "%s.csv" % name) if create and not os.path.exists(path): f = file(path, "a").close() return MapFileSource(name, "Static file", path) open_source_by_name = Callable(_open_source_by_name) def __parse_line(self, line): try: id, icon, lat, lon, alt, comment, show = line.split(",", 6) except Exception, e: raise MapSourcePointError(str(e)) if alt: alt = float(alt) else: alt = 0.0 point = MapStation(id, float(lat), float(lon), float(alt), comment) point.set_visible(show.upper().strip() == "TRUE") if icon and icon != "None": point.set_icon_from_aprs_sym(icon) return point def save(self): self._need_save = 0 f = file(self._fn, "w") for point in self.get_points(): f.write("%s,%s,%f,%f,%f,%s,%s%s" % (point.get_name(), point.get_aprs_symbol(), point.get_latitude(), point.get_longitude(), point.get_altitude(), point.get_comment(), point.get_visible(), os.linesep)) f.close() def __init__(self, name, description, fn, create=False): MapSource.__init__(self, name, description) self._fn = fn self.__need_save = 0 try: input = file(fn) except Exception, e: msg = "Failed to open %s: %s" % (fn, e) print msg raise MapSourceFailedToConnect(msg) lines = input.readlines() for line in lines: try: point = self.__parse_line(line) except Exception, e: print "Failed to parse: %s" % e continue self._points[point.get_name()] = point def get_filename(self): return self._fn class MapUSGSRiverSource(MapSource): def _open_source_by_name(config, name): if not config.has_section("rivers"): return None if not config.has_option("rivers", name): return None sites = tuple(config.get("rivers", name).split(",")) try: _name = config.get("rivers", "%s.label" % name) except Exception, e: print "No option %s.label" % name print e _name = name return MapUSGSRiverSource(_name, "NWIS Rivers", *sites) open_source_by_name = Callable(_open_source_by_name) def _enumerate(config): if not config.has_section("rivers"): return [] options = config.options("rivers") return [x for x in options if not x.endswith(".label")] enumerate = Callable(_enumerate) def packed_name(self): name = [] for i in self.get_name(): if (ord(i) > ord("A") and ord(i) < ord("Z")) or\ (ord(i) > ord("a") and ord(i) < ord("z")): name.append(i) return "".join(name) def _point_updated(self, point, foo): if not self._points.has_key(point.get_name()): self._points[point.get_name()] = point self.emit("point-added", point) else: self.emit("point-updated", point) def __init__(self, name, description, *sites): MapSource.__init__(self, name, description) self.__sites = sites self._mutable = False for site in sites: point = MapUSGSRiver(site) point.connect("updated", self._point_updated) def get_sites(self): return self.__sites class MapNBDCBuoySource(MapSource): def _open_source_by_name(config, name): if not config.has_section("buoys"): return None if not config.has_option("buoys", name): return None _sites = config.get("buoys", name).split(",") try: _name = config.get("buoys", "%s.label" % name) except Exception, e: print "No option %s.label" % name print e _name = name sites = tuple([x for x in _sites]) return MapNBDCBuoySource(_name, "NBDC Buoys", *sites) open_source_by_name = Callable(_open_source_by_name) def _enumerate(config): if not config.has_section("buoys"): return [] options = config.options("buoys") return [x for x in options if not x.endswith(".label")] enumerate = Callable(_enumerate) def packed_name(self): name = [] for i in self.get_name(): if (ord(i) > ord("A") and ord(i) < ord("Z")) or\ (ord(i) > ord("a") and ord(i) < ord("z")): name.append(i) return "".join(name) def _point_updated(self, point, foo): if not self._points.has_key(point.get_name()): self._points[point.get_name()] = point self.emit("point-added", point) else: self.emit("point-updated", point) def __init__(self, name, description, *buoys): MapSource.__init__(self, name, description) self.__buoys = buoys self._mutable = False for buoy in buoys: point = MapNBDCBuoy(buoy) point.connect("updated", self._point_updated) def get_buoys(self): return self.__buoys d-rats-0.3.3/d_rats/mapdisplay.py000066400000000000000000001456731160617671700167340ustar00rootroot00000000000000#!/usr/bin/python import os import math import urllib import time import random import shutil import tempfile import threading import copy import gtk import gobject import mainapp import platform import miscwidgets import inputdialog import utils import geocode_ui import map_sources import map_source_editor import signals from ui.main_common import ask_for_confirmation from gps import GPSPosition, distance, value_with_units, DPRS_TO_APRS CROSSHAIR = "+" COLORS = ["red", "green", "cornflower blue", "pink", "orange", "grey"] BASE_DIR = None def set_base_dir(basedir): global BASE_DIR BASE_DIR = basedir CONFIG = None CONNECTED = True MAX_TILE_LIFE = 0 PROXY = None def set_connected(connected): global CONNECTED CONNECTED = connected def set_tile_lifetime(lifetime): global MAX_TILE_LIFE MAX_TILE_LIFE = lifetime def set_proxy(proxy): global PROXY PROXY = proxy def fetch_url(url, local): global CONNECTED global PROXY if not CONNECTED: raise Exception("Not connected") if PROXY: proxies = {"http" : PROXY} else: proxies = None data = urllib.urlopen(url, proxies=proxies) local_file = file(local, "wb") d = data.read() local_file.write(d) data.close() local_file.close() class MarkerEditDialog(inputdialog.FieldDialog): def __init__(self): inputdialog.FieldDialog.__init__(self, title=_("Add Marker")) self.icons = [] for sym in sorted(DPRS_TO_APRS.values()): icon = utils.get_icon(sym) if icon: self.icons.append((icon, sym)) self.add_field(_("Group"), miscwidgets.make_choice([], True)) self.add_field(_("Name"), gtk.Entry()) self.add_field(_("Latitude"), miscwidgets.LatLonEntry()) self.add_field(_("Longitude"), miscwidgets.LatLonEntry()) self.add_field(_("Lookup"), gtk.Button("By Address")) self.add_field(_("Comment"), gtk.Entry()) self.add_field(_("Icon"), miscwidgets.make_pixbuf_choice(self.icons)) self._point = None def set_groups(self, groups, group=None): grpsel = self.get_field(_("Group")) for grp in groups: grpsel.append_text(grp) if group is not None: grpsel.child.set_text(group) grpsel.set_sensitive(False) else: grpsel.child.set_text(_("Misc")) def get_group(self): return self.get_field(_("Group")).child.get_text() def set_point(self, point): self.get_field(_("Name")).set_text(point.get_name()) self.get_field(_("Latitude")).set_text("%.4f" % point.get_latitude()) self.get_field(_("Longitude")).set_text("%.4f" % point.get_longitude()) self.get_field(_("Comment")).set_text(point.get_comment()) iconsel = self.get_field(_("Icon")) if isinstance(point, map_sources.MapStation): symlist = [y for x,y in self.icons] try: iidx = symlist.index(point.get_aprs_symbol()) iconsel.set_active(iidx) except ValueError: print "No such symbol `%s'" % point.get_aprs_symbol() else: iconsel.set_sensitive(False) self._point = point def get_point(self): name = self.get_field(_("Name")).get_text() lat = self.get_field(_("Latitude")).value() lon = self.get_field(_("Longitude")).value() comment = self.get_field(_("Comment")).get_text() idx = self.get_field(_("Icon")).get_active() self._point.set_name(name) self._point.set_latitude(lat) self._point.set_longitude(lon) self._point.set_comment(comment) if isinstance(self._point, map_sources.MapStation): self._point.set_icon_from_aprs_sym(self.icons[idx][1]) return self._point # These functions taken from: # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames def deg2num(lat_deg, lon_deg, zoom): lat_rad = lat_deg * math.pi / 180.0 n = 2.0 ** zoom xtile = int((lon_deg + 180.0) / 360.0 * n) ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) return(xtile, ytile) def num2deg(xtile, ytile, zoom): n = 2.0 ** zoom lon_deg = xtile / n * 360.0 - 180.0 lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) lat_deg = lat_rad * 180.0 / math.pi return(lat_deg, lon_deg) class MapTile(object): def path_els(self): return deg2num(self.lat, self.lon, self.zoom) def tile_edges(self): n, w = num2deg(self.x, self.y, self.zoom) s, e = num2deg(self.x+1, self.y+1, self.zoom) return (s, w, n, e) def lat_range(self): s, w, n, e = self.tile_edges() return (n, s) def lon_range(self): s, w, n, e = self.tile_edges() return (w, e) def path(self): return "%d/%d/%d.png" % (self.zoom, self.x, self.y) def _local_path(self): path = os.path.join(self.dir, self.path()) if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) return path def is_local(self): if MAX_TILE_LIFE == 0 or not CONNECTED: return os.path.exists(self._local_path()) else: try: ts = os.stat(self._local_path()).st_mtime return (time.time() - ts) < MAX_TILE_LIFE except OSError: return False def fetch(self): if not self.is_local(): for i in range(10): url = self.remote_path() try: fetch_url(url, self._local_path()) return True except Exception, e: print "[%i] Failed to fetch `%s': %s" % (i, url, e) return False else: return True def _thread(self, cb, *args): if self.fetch(): fname = self._local_path() else: fname = None gobject.idle_add(cb, fname, *args) def threaded_fetch(self, cb, *args): _args = (cb,) + args t = threading.Thread(target=self._thread, args=_args) t.setDaemon(True) t.start() def local_path(self): path = self._local_path() self.fetch() return path def remote_path(self): return "http://tile.openstreetmap.org/%s" % (self.path()) def __add__(self, count): (x, y) = count return MapTile(self.x+x, self.y+y, self.zoom) def __sub__(self, tile): return (self.x - tile.x, self.y - tile.y) def __contains__(self, point): (lat, lon) = point # FIXME for non-western! (lat_max, lat_min) = self.lat_range() (lon_min, lon_max) = self.lon_range() lat_match = (lat < lat_max and lat > lat_min) lon_match = (lon < lon_max and lon > lon_min) return lat_match and lon_match def __init__(self, lat, lon, zoom): self.zoom = zoom if isinstance(lat, int) and isinstance(lon, int): self.x = lat self.y = lon self.lat, self.lon = num2deg(self.x, self.y, self.zoom) else: self.lat = lat self.lon = lon self.x, self.y = deg2num(self.lat, self.lon, self.zoom) if BASE_DIR: self.dir = BASE_DIR else: p = platform.get_platform() self.dir = os.path.join(p.config_dir(), "maps") if not os.path.isdir(self.dir): os.mkdir(self.dir) def __str__(self): return "%.4f,%.4f (%i,%i)" % (self.lat, self.lon, self.x, self.y) class LoadContext(object): pass class MapWidget(gtk.DrawingArea): __gsignals__ = { "redraw-markers" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "new-tiles-loaded" : (gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ()), } def draw_text_marker_at(self, x, y, text, color="yellow"): gc = self.get_style().black_gc if self.zoom < 12: size = 'size="x-small"' elif self.zoom < 14: size = 'size="small"' else: size = '' text = utils.filter_to_ascii(text) pl = self.create_pango_layout("") markup = '%s' % (size, color, text) pl.set_markup(markup) self.window.draw_layout(gc, int(x), int(y), pl) def draw_image_at(self, x, y, pb): gc = self.get_style().black_gc self.window.draw_pixbuf(gc, pb, 0, 0, int(x), int(y)) return pb.get_height() def draw_cross_marker_at(self, x, y): width = 2 cm = self.window.get_colormap() color = cm.alloc_color("red") gc = self.window.new_gc(foreground=color, line_width=width) x = int(x) y = int(y) self.window.draw_lines(gc, [(x, y-5), (x, y+5)]) self.window.draw_lines(gc, [(x-5, y), (x+5, y)]) def latlon2xy(self, lat, lon): y = 1- ((lat - self.lat_min) / (self.lat_max - self.lat_min)) x = 1- ((lon - self.lon_min) / (self.lon_max - self.lon_min)) x *= (self.tilesize * self.width) y *= (self.tilesize * self.height) y += self.lat_fudge return (x, y) def xy2latlon(self, x, y): y -= self.lat_fudge lon = 1 - (float(x) / (self.tilesize * self.width)) lat = 1 - (float(y) / (self.tilesize * self.height)) lat = (lat * (self.lat_max - self.lat_min)) + self.lat_min lon = (lon * (self.lon_max - self.lon_min)) + self.lon_min return lat, lon def draw_marker(self, label, lat, lon, img=None): color = "red" try: x, y = self.latlon2xy(lat, lon) except ZeroDivisionError: return if label == CROSSHAIR: self.draw_cross_marker_at(x, y) else: if img: y += (4 + self.draw_image_at(x, y, img)) self.draw_text_marker_at(x, y, label, color) def expose(self, area, event): if len(self.map_tiles) == 0: self.load_tiles() gc = self.get_style().black_gc self.window.draw_drawable(gc, self.pixmap, 0, 0, 0, 0, -1, -1) self.emit("redraw-markers") def calculate_bounds(self): center = MapTile(self.lat, self.lon, self.zoom) topleft = center + (-2, -2) botright = center + (2, 2) (self.lat_min, _, _, self.lon_min) = botright.tile_edges() (_, self.lon_max, self.lat_max, _) = topleft.tile_edges() # I have no idea why, but for some reason we can calculate the # longitude (x) just fine, but not the latitude (y). The result # of either latlon2xy() or tile_edges() is bad, which causes the # y calculation of a given latitude to be off by some amount. # The amount grows large at small values of zoom (zoomed out) and # starts to seriously affect the correctness of marker placement. # Until I figure out why that is, we calculate a fudge factor below. # # To do this, we ask the center tile for its NW corner's # coordinates. We then ask latlon2xy() (with fudge of zero) what # the corresponding x,y is. Since we know what the correct value # should be, we record the offset and use that to shift the y in # further calculations for this zoom level. self.lat_fudge = 0 s, w, n, e = center.tile_edges() x, y = self.latlon2xy(n, w) self.lat_fudge = ((self.height / 2) * self.tilesize) - y if False: print "------ Bounds Calculation ------" print "Center tile should be at %i,%i" % (\ (self.height/2) * self.tilesize, (self.width/2) * self.tilesize) print "We calculate it based on Lat,Lon to be %i, %i" % (x, y) print "--------------------------------" print "Latitude Fudge Factor: %i (zoom %i)" % (self.lat_fudge, self.zoom) def broken_tile(self): if self.__broken_tile: return self.__broken_tile broken = [ "48 16 3 1", " c #FFFFFFFFFFFF", "x c #FFFF00000000", "X c #000000000000", "xx xx XX X XXX ", " xx xx X X X X X ", " xx xx X X X X X ", " xx xx X X X X X ", " xx xx X X X X X ", " xx xx X X X X X ", " xx xx X XX XXX ", " xxx ", " xxx ", " xx xx XXXX XX XXXXX XX ", " xx xx X X X X X X X ", " xx xx X X X X X X X ", " xx xx X X X X X X X ", " xx xx X X XXXXXX X XXXXXX ", " xx xx X X X X X X X ", "xx xx XXXX X X X X X " ] # return gtk.gdk.pixbuf_new_from_xpm_data(broken) pm = gtk.gdk.pixmap_create_from_xpm_d(self.window, None, broken)[0] pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize) pb.fill(0xffffffff) x = y = (self.tilesize / 2) pb.get_from_drawable(pm, pm.get_colormap(), 0, 0, x, y, -1, -1) self.__broken_tile = pb return pb def draw_tile(self, path, x, y, ctx=None): if ctx and ctx.zoom != self.zoom: # Zoom level has chnaged, so don't do anything return gc = self.pixmap.new_gc() if path: try: pb = gtk.gdk.pixbuf_new_from_file(path) except Exception, e: utils.log_exception() pb = self.broken_tile() else: pb = self.broken_tile() if ctx: ctx.loaded_tiles += 1 frac = float(ctx.loaded_tiles) / float(ctx.total_tiles) if ctx.loaded_tiles == ctx.total_tiles: self.status(0.0, "") else: self.status(frac, _("Loaded") + " %.0f%%" % (frac * 100.0)) self.pixmap.draw_pixbuf(gc, pb, 0, 0, x, y, -1, -1) self.queue_draw() @utils.run_gtk_locked def draw_tile_locked(self, *args): self.draw_tile(*args) def load_tiles(self): self.map_tiles = [] ctx = LoadContext() ctx.loaded_tiles = 0 ctx.total_tiles = self.width * self.height ctx.zoom = self.zoom center = MapTile(self.lat, self.lon, self.zoom) delta_h = self.height / 2 delta_w = self.width / 2 count = 0 total = self.width * self.height if not self.window: # Window is not loaded, thus can't load tiles return try: self.pixmap = gtk.gdk.Pixmap(self.window, self.width * self.tilesize, self.height * self.tilesize) except Exception, e: # Window is not loaded, thus can't load tiles return gc = self.pixmap.new_gc() for i in range(0, self.width): for j in range(0, self.height): tile = center + (i - delta_w, j - delta_h) if not tile.is_local(): message = _("Retrieving") else: message = _("Loading") if tile.is_local(): path = tile._local_path() self.draw_tile(tile._local_path(), self.tilesize * i, self.tilesize * j, ctx) else: self.draw_tile(None, self.tilesize * i, self.tilesize * j) tile.threaded_fetch(self.draw_tile_locked, self.tilesize * i, self.tilesize * j, ctx) self.map_tiles.append(tile) count += 1 self.calculate_bounds() self.emit("new-tiles-loaded") def export_to(self, filename, bounds=None): if not bounds: x = 0 y = 0 bounds = (0,0,-1,-1) width = self.tilesize * self.width height = self.tilesize * self.height else: x = bounds[0] y = bounds[1] width = bounds[2] - bounds[0] height = bounds[3] - bounds[1] pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height) pb.get_from_drawable(self.pixmap, self.pixmap.get_colormap(), x, y, 0, 0, width, height) pb.save(filename, "png") def __init__(self, width, height, tilesize=256, status=None): gtk.DrawingArea.__init__(self) self.__broken_tile = None self.height = height self.width = width self.tilesize = tilesize self.status = status self.lat = 0 self.lon = 0 self.zoom = 1 self.lat_max = self.lat_min = 0 self.lon_max = self.lon_min = 0 self.map_tiles = [] self.set_size_request(self.tilesize * self.width, self.tilesize * self.height) self.connect("expose-event", self.expose) def set_center(self, lat, lon): self.lat = lat self.lon = lon self.map_tiles = [] self.queue_draw() def get_center(self): return (self.lat, self.lon) def set_zoom(self, zoom): if zoom > 17 or zoom == 1: return self.zoom = zoom self.map_tiles = [] self.queue_draw() def get_zoom(self): return self.zoom def scale(self, x, y, pixels=128): shift = 15 tick = 5 #rect = gtk.gdk.Rectangle(x-pixels,y-shift-tick,x,y) #self.window.invalidate_rect(rect, True) (lat_a, lon_a) = self.xy2latlon(self.tilesize, self.tilesize) (lat_b, lon_b) = self.xy2latlon(self.tilesize * 2, self.tilesize) # width of one tile d = distance(lat_a, lon_a, lat_b, lon_b) * (float(pixels) / self.tilesize) dist = value_with_units(d) color = self.window.get_colormap().alloc_color("black") gc = self.window.new_gc(line_width=1, foreground=color) self.window.draw_line(gc, x-pixels, y-shift, x, y-shift) self.window.draw_line(gc, x-pixels, y-shift, x-pixels, y-shift-tick) self.window.draw_line(gc, x, y-shift, x, y-shift-tick) self.window.draw_line(gc, x-(pixels/2), y-shift, x-(pixels/2), y-shift-tick) pl = self.create_pango_layout("") pl.set_markup("%s" % dist) self.window.draw_layout(gc, x-pixels, y-shift, pl) def point_is_visible(self, lat, lon): for i in self.map_tiles: if (lat, lon) in i: return True return False class MapWindow(gtk.Window): __gsignals__ = { "reload-sources" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "user-send-chat" : signals.USER_SEND_CHAT, "get-station-list" : signals.GET_STATION_LIST, } _signals = {"user-send-chat" : None, "get-station-list" : None, } def zoom(self, widget, frame): adj = widget.get_adjustment() self.map.set_zoom(int(adj.value)) frame.set_label(_("Zoom") + " (%i)" % int(adj.value)) def make_zoom_controls(self): box = gtk.HBox(False, 3) box.set_border_width(3) box.show() l = gtk.Label(_("Min")) l.show() box.pack_start(l, 0,0,0) adj = gtk.Adjustment(value=14, lower=2, upper=17, step_incr=1, page_incr=3) sb = gtk.HScrollbar(adj) sb.show() box.pack_start(sb, 1,1,1) l = gtk.Label(_("Max")) l.show() box.pack_start(l, 0,0,0) frame = gtk.Frame(_("Zoom")) frame.set_label_align(0.5, 0.5) frame.set_size_request(150, 50) frame.show() frame.add(box) sb.connect("value-changed", self.zoom, frame) return frame def toggle_show(self, group, *vals): if group: station = vals[1] else: group = vals[1] station = None for src in self.map_sources: if group != src.get_name(): continue if station: try: point = src.get_point_by_name(station) except KeyError: continue point.set_visible(vals[0]) self.add_point_visible(point) else: src.set_visible(vals[0]) for point in src.get_points(): point.set_visible(vals[0]) self.update_point(src, point) src.save() break self.map.queue_draw() def marker_mh(self, _action, id, group): action = _action.get_name() if action == "delete": print "Deleting %s/%s" % (group, id) for source in self.map_sources: if source.get_name() == group: if not source.get_mutable(): return point = source.get_point_by_name(id) source.del_point(point) source.save() elif action == "edit": for source in self.map_sources: if source.get_name() == group: break if not source.get_mutable(): return if not source: return for point in source.get_points(): if point.get_name() == id: break if not point: return _point = point.dup() upoint, foo = self.prompt_to_set_marker(point, source.get_name()) if upoint: self.del_point(source, _point) self.add_point(source, upoint) source.save() def _make_marker_menu(self, store, iter): menu_xml = """ """ ag = gtk.ActionGroup("menu") try: id, = store.get(iter, 1) group, = store.get(store.iter_parent(iter), 1) except TypeError: id = group = None edit = gtk.Action("edit", _("Edit"), None, None) edit.connect("activate", self.marker_mh, id, group) if not id: edit.set_sensitive(False) ag.add_action(edit) delete = gtk.Action("delete", _("Delete"), None, None) delete.connect("activate", self.marker_mh, id, group) ag.add_action(delete) center = gtk.Action("center", _("Center on this"), None, None) center.connect("activate", self.marker_mh, id, group) # This isn't implemented right now, because I'm lazy center.set_sensitive(False) ag.add_action(center) uim = gtk.UIManager() uim.insert_action_group(ag, 0) uim.add_ui_from_string(menu_xml) return uim.get_widget("/menu") def make_marker_popup(self, _, view, event): if event.button != 3: return if event.window == view.get_bin_window(): x, y = event.get_coords() pathinfo = view.get_path_at_pos(int(x), int(y)) if pathinfo is None: return else: view.set_cursor_on_cell(pathinfo[0]) (store, iter) = view.get_selection().get_selected() menu = self._make_marker_menu(store, iter) if menu: menu.popup(None, None, None, event.button, event.time) def make_marker_list(self): cols = [(gobject.TYPE_BOOLEAN, _("Show")), (gobject.TYPE_STRING, _("Station")), (gobject.TYPE_FLOAT, _("Latitude")), (gobject.TYPE_FLOAT, _("Longitude")), (gobject.TYPE_FLOAT, _("Distance")), (gobject.TYPE_FLOAT, _("Direction")), ] self.marker_list = miscwidgets.TreeWidget(cols, 1, parent=False) self.marker_list.toggle_cb.append(self.toggle_show) self.marker_list.connect("click-on-list", self.make_marker_popup) self.marker_list._view.connect("row-activated", self.recenter_cb) def render_station(col, rend, model, iter): parent = model.iter_parent(iter) if not parent: parent = iter group = model.get_value(parent, 1) if self.colors.has_key(group): rend.set_property("foreground", self.colors[group]) c = self.marker_list._view.get_column(1) c.set_expand(True) c.set_min_width(150) r = c.get_cell_renderers()[0] c.set_cell_data_func(r, render_station) def render_coord(col, rend, model, iter, cnum): if model.iter_parent(iter): rend.set_property('text', "%.4f" % model.get_value(iter, cnum)) else: rend.set_property('text', '') for col in [2, 3]: c = self.marker_list._view.get_column(col) r = c.get_cell_renderers()[0] c.set_cell_data_func(r, render_coord, col) def render_dist(col, rend, model, iter, cnum): if model.iter_parent(iter): rend.set_property('text', "%.2f" % model.get_value(iter, cnum)) else: rend.set_property('text', '') for col in [4, 5]: c = self.marker_list._view.get_column(col) r = c.get_cell_renderers()[0] c.set_cell_data_func(r, render_dist, col) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add(self.marker_list.packable()) sw.set_size_request(-1, 150) sw.show() return sw def refresh_marker_list(self, group=None): (lat, lon) = self.map.get_center() center = GPSPosition(lat=lat, lon=lon) for item in self.marker_list.get_values(group): try: _parent, children = item except ValueError: # Empty group continue parent = _parent[1] for child in children: this = GPSPosition(lat=child[2], lon=child[3]) dist = center.distance_from(this) bear = center.bearing_to(this) self.marker_list.set_item(parent, child[0], child[1], child[2], child[3], dist, bear) def make_track(self): def toggle(cb, mw): mw.tracking_enabled = cb.get_active() cb = gtk.CheckButton(_("Track center")) cb.connect("toggled", toggle, self) cb.show() return cb def clear_map_cache(self): d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO) d.set_property("text", _("Are you sure you want to clear your map cache?")) r = d.run() d.destroy() if r == gtk.RESPONSE_YES: dir = os.path.join(platform.get_platform().config_dir(), "maps") shutil.rmtree(dir, True) self.map.queue_draw() def printable_map(self, bounds=None): p = platform.get_platform() f = tempfile.NamedTemporaryFile() fn = f.name f.close() mf = "%s.png" % fn hf = "%s.html" % fn ts = time.strftime("%H:%M:%S %d-%b-%Y") station_map = _("Station map") generated_at = _("Generated at") html = """

D-RATS %s

%s %s
""" % (station_map, generated_at, ts, mf) self.map.export_to(mf, bounds) f = file(hf, "w") f.write(html) f.close() p.open_html_file(hf) def save_map(self, bounds=None): p = platform.get_platform() f = p.gui_save_file(default_name="map_%s.png" % \ time.strftime("%m%d%Y%_H%M%S")) if not f: return if not f.endswith(".png"): f += ".png" self.map.export_to(f, bounds) def get_visible_bounds(self): ha = self.sw.get_hadjustment() va = self.sw.get_vadjustment() return (int(ha.value), int(va.value), int(ha.value + ha.page_size), int(va.value + va.page_size)) def mh(self, _action): action = _action.get_name() if action == "refresh": self.map_tiles = [] self.map.queue_draw() elif action == "clearcache": self.clear_map_cache() elif action == "save": self.save_map() elif action == "savevis": self.save_map(self.get_visible_bounds()) elif action == "printable": self.printable_map() elif action == "printablevis": self.printable_map(self.get_visible_bounds()) elif action == "editsources": srced = map_source_editor.MapSourcesEditor(self.config) srced.run() srced.destroy() self.emit("reload-sources") def make_menu(self): menu_xml = """ """ actions = [('map', None, "_" + _("Map"), None, None, self.mh), ('refresh', None, "_" + _("Refresh"), None, None, self.mh), ('clearcache', None, "_" + _("Clear Cache"), None, None, self.mh), ('editsources', None, _("Edit Sources"), None, None, self.mh), ('export', None, "_" + _("Export"), None, None, self.mh), ('printable', None, "_" + _("Printable"), "p", None, self.mh), ('printablevis', None, _("Printable (visible area)"), "P", None, self.mh), ('save', None, "_" + _("Save Image"), "s", None, self.mh), ('savevis', None, _('Save Image (visible area)'), "S", None, self.mh), ] uim = gtk.UIManager() self.menu_ag = gtk.ActionGroup("MenuBar") self.menu_ag.add_actions(actions) uim.insert_action_group(self.menu_ag, 0) menuid = uim.add_ui_from_string(menu_xml) self._accel_group = uim.get_accel_group() return uim.get_widget("/MenuBar") def make_controls(self): vbox = gtk.VBox(False, 2) vbox.pack_start(self.make_zoom_controls(), 0,0,0) vbox.pack_start(self.make_track(), 0,0,0) vbox.show() return vbox def make_bottom_pane(self): box = gtk.HBox(False, 2) box.pack_start(self.make_marker_list(), 1,1,1) box.pack_start(self.make_controls(), 0,0,0) box.show() return box def scroll_to_center(self, widget): a = widget.get_vadjustment() a.set_value((a.upper - a.page_size) / 2) a = widget.get_hadjustment() a.set_value((a.upper - a.page_size) / 2) def center_on(self, lat, lon): ha = self.sw.get_hadjustment() va = self.sw.get_vadjustment() x, y = self.map.latlon2xy(lat, lon) ha.set_value(x - (ha.page_size / 2)) va.set_value(y - (va.page_size / 2)) def status(self, frac, message): self.sb_prog.set_text(message) self.sb_prog.set_fraction(frac) def recenter(self, lat, lon): self.map.set_center(lat, lon) self.map.load_tiles() self.refresh_marker_list() self.center_on(lat, lon) self.map.queue_draw() def refresh(self): self.map.load_tiles() def prompt_to_set_marker(self, point, group=None): def do_address(button, latw, lonw, namew): dlg = geocode_ui.AddressAssistant() r = dlg.run() if r == gtk.RESPONSE_OK: if not namew.get_text(): namew.set_text(dlg.place) latw.set_text("%.5f" % dlg.lat) lonw.set_text("%.5f" % dlg.lon) d = MarkerEditDialog() sources = [] for src in self.map_sources: if src.get_mutable(): sources.append(src.get_name()) d.set_groups(sources, group) d.set_point(point) r = d.run() if r == gtk.RESPONSE_OK: point = d.get_point() group = d.get_group() d.destroy() if r == gtk.RESPONSE_OK: return point, group else: return None, None def prompt_to_send_loc(self, _lat, _lon): d = inputdialog.FieldDialog(title=_("Broadcast Location")) d.add_field(_("Callsign"), gtk.Entry(8)) d.add_field(_("Description"), gtk.Entry(20)) d.add_field(_("Latitude"), miscwidgets.LatLonEntry()) d.add_field(_("Longitude"), miscwidgets.LatLonEntry()) d.get_field(_("Latitude")).set_text("%.4f" % _lat) d.get_field(_("Longitude")).set_text("%.4f" % _lon) while d.run() == gtk.RESPONSE_OK: try: call = d.get_field(_("Callsign")).get_text() desc = d.get_field(_("Description")).get_text() lat = d.get_field(_("Latitude")).get_text() lon = d.get_field(_("Longitude")).get_text() fix = GPSPosition(lat=lat, lon=lon, station=call) fix.comment = desc for port in self.emit("get-station-list").keys(): self.emit("user-send-chat", "CQCQCQ", port, fix.to_NMEA_GGA(), True) break except Exception, e: utils.log_exception() ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=d) ed.set_property("text", _("Invalid value") + ": %s" % e) ed.run() ed.destroy() d.destroy() def recenter_cb(self, view, path, column, data=None): model = view.get_model() if model.iter_parent(model.get_iter(path)) == None: return items = self.marker_list.get_selected() self.center_mark = items[1] self.recenter(items[2], items[3]) self.sb_center.pop(self.STATUS_CENTER) self.sb_center.push(self.STATUS_CENTER, _("Center") + ": %s" % self.center_mark) def make_popup(self, vals): def _an(cap): return cap.replace(" ", "_") xml = "" for action in [_an(x) for x in self._popup_items.keys()]: xml += "\n" % action xml = """ %s """ % xml ag = gtk.ActionGroup("menu") t = gtk.Action("title", "%.4f,%.4f" % (vals["lat"], vals["lon"]), None, None) t.set_sensitive(False) ag.add_action(t) for name, handler in self._popup_items.items(): action = gtk.Action(_an(name), name, None, None) action.connect("activate", handler, vals) ag.add_action(action) uim = gtk.UIManager() uim.insert_action_group(ag, 0) uim.add_ui_from_string(xml) return uim.get_widget("/menu") def mouse_click_event(self, widget, event): x,y = event.get_coords() ha = widget.get_hadjustment() va = widget.get_vadjustment() mx = x + int(ha.get_value()) my = y + int(va.get_value()) lat, lon = self.map.xy2latlon(mx, my) print "Button %i at %i,%i" % (event.button, mx, my) if event.button == 3: vals = { "lat" : lat, "lon" : lon, "x" : mx, "y" : my } menu = self.make_popup(vals) if menu: menu.popup(None, None, None, event.button, event.time) elif event.type == gtk.gdk.BUTTON_PRESS: print "Clicked: %.4f,%.4f" % (lat, lon) # The crosshair marker has been missing since 0.3.0 #self.set_marker(GPSPosition(station=CROSSHAIR, # lat=lat, lon=lon)) elif event.type == gtk.gdk._2BUTTON_PRESS: print "Recenter on %.4f, %.4f" % (lat,lon) self.recenter(lat, lon) def mouse_move_event(self, widget, event): if not self.__last_motion: gobject.timeout_add(100, self._mouse_motion_handler) self.__last_motion = (time.time(), event.x, event.y) def _mouse_motion_handler(self): if self.__last_motion == None: return False t, x, y = self.__last_motion if (time.time() - t) < 0.5: self.info_window.hide() return True lat, lon = self.map.xy2latlon(x, y) ha = self.sw.get_hadjustment() va = self.sw.get_vadjustment() mx = x - int(ha.get_value()) my = y - int(va.get_value()) hit = False for source in self.map_sources: if not source.get_visible(): continue for point in source.get_points(): if not point.get_visible(): continue try: _x, _y = self.map.latlon2xy(point.get_latitude(), point.get_longitude()) except ZeroDivisionError: continue dx = abs(x - _x) dy = abs(y - _y) if dx < 20 and dy < 20: hit = True date = time.ctime(point.get_timestamp()) text = "Station: %s" % point.get_name() + \ "\nLatitude: %.5f" % point.get_latitude() + \ "\nLongitude: %.5f"% point.get_longitude() + \ "\nLast update: %s" % date text += "\nInfo: %s" % point.get_comment() label = gtk.Label() label.set_markup(text) label.show() for child in self.info_window.get_children(): self.info_window.remove(child) self.info_window.add(label) posx, posy = self.get_position() posx += mx + 10 posy += my - 10 self.info_window.move(int(posx), int(posy)) self.info_window.show() break if not hit: self.info_window.hide() self.sb_coords.pop(self.STATUS_COORD) self.sb_coords.push(self.STATUS_COORD, "%.4f, %.4f" % (lat, lon)) self.__last_motion = None return False def ev_destroy(self, widget, data=None): self.hide() return True def ev_delete(self, widget, event, data=None): self.hide() return True def update_gps_status(self, string): self.sb_gps.pop(self.STATUS_GPS) self.sb_gps.push(self.STATUS_GPS, string) def add_point_visible(self, point): if point in self.points_visible: self.points_visible.remove(point) if self.map.point_is_visible(point.get_latitude(), point.get_longitude()): if point.get_visible(): self.points_visible.append(point) return True else: return False else: return False def update_point(self, source, point): (lat, lon) = self.map.get_center() center = GPSPosition(*self.map.get_center()) this = GPSPosition(point.get_latitude(), point.get_longitude()) try: self.marker_list.set_item(source.get_name(), point.get_visible(), point.get_name(), point.get_latitude(), point.get_longitude(), center.distance_from(this), center.bearing_to(this)) except Exception, e: if str(e) == "Item not found": # this is evil print "Adding point instead of updating" return self.add_point(source, point) self.add_point_visible(point) self.map.queue_draw() def add_point(self, source, point): (lat, lon) = self.map.get_center() center = GPSPosition(*self.map.get_center()) this = GPSPosition(point.get_latitude(), point.get_longitude()) self.marker_list.add_item(source.get_name(), point.get_visible(), point.get_name(), point.get_latitude(), point.get_longitude(), center.distance_from(this), center.bearing_to(this)) self.add_point_visible(point) self.map.queue_draw() def del_point(self, source, point): self.marker_list.del_item(source.get_name(), point.get_name()) if point in self.points_visible: self.points_visible.remove(point) self.map.queue_draw() def get_map_source(self, name): for source in self.get_map_sources(): if source.get_name() == name: return source return None def add_map_source(self, source): self.map_sources.append(source) self.marker_list.add_item(None, source.get_visible(), source.get_name(), 0, 0, 0, 0) for point in source.get_points(): self.add_point(source, point) #source.connect("point-updated", self.update_point) source.connect("point-added", self.add_point) source.connect("point-deleted", self.del_point) source.connect("point-updated", self.maybe_recenter_on_updated_point) def update_points_visible(self): for src in self.map_sources: for point in src.get_points(): self.update_point(src, point) self.map.queue_draw() def maybe_recenter_on_updated_point(self, source, point): if point.get_name() == self.center_mark and \ self.tracking_enabled: print "Center updated" self.recenter(point.get_latitude(), point.get_longitude()) self.update_point(source, point) def clear_map_sources(self): self.marker_list.clear() self.map_sources = [] self.points_visible = [] self.update_points_visible() def get_map_sources(self): return self.map_sources def redraw_markers(self, map): for point in self.points_visible: map.draw_marker(point.get_name(), point.get_latitude(), point.get_longitude(), point.get_icon()) def __init__(self, config, *args): gtk.Window.__init__(self, *args) self.config = config self.STATUS_COORD = 0 self.STATUS_CENTER = 1 self.STATUS_GPS = 2 self.center_mark = None self.tracking_enabled = False tiles = 5 self.points_visible = [] self.map_sources = [] self.map = MapWidget(tiles, tiles, status=self.status) self.map.show() self.map.connect("redraw-markers", self.redraw_markers) self.map.connect("new-tiles-loaded", lambda m: self.update_points_visible()) box = gtk.VBox(False, 2) self.menubar = self.make_menu() self.menubar.show() box.pack_start(self.menubar, 0,0,0) self.add_accel_group(self._accel_group) self.sw = gtk.ScrolledWindow() self.sw.add_with_viewport(self.map) self.sw.show() def pre_scale(sw, event, mw): ha = mw.sw.get_hadjustment() va = mw.sw.get_vadjustment() px = ha.get_value() + ha.page_size py = va.get_value() + va.page_size rect = gtk.gdk.Rectangle(int(ha.get_value()), int(va.get_value()), int(py), int(py)) mw.map.window.invalidate_rect(rect, True) @utils.run_gtk_locked def _scale(sw, event, mw): ha = mw.sw.get_hadjustment() va = mw.sw.get_vadjustment() px = ha.get_value() + ha.page_size py = va.get_value() + va.page_size pm = mw.map.scale(int(px) - 5, int(py)) def scale(sw, event, mw): gobject.idle_add(_scale, sw, event, mw) self.sw.connect("expose-event", pre_scale, self) self.sw.connect_after("expose-event", scale, self) self.__last_motion = None self.map.add_events(gtk.gdk.POINTER_MOTION_MASK) self.map.connect("motion-notify-event", self.mouse_move_event) self.sw.connect("button-press-event", self.mouse_click_event) self.sw.connect('realize', self.scroll_to_center) hbox = gtk.HBox(False, 2) self.sb_coords = gtk.Statusbar() self.sb_coords.show() self.sb_coords.set_has_resize_grip(False) self.sb_center = gtk.Statusbar() self.sb_center.show() self.sb_center.set_has_resize_grip(False) self.sb_gps = gtk.Statusbar() self.sb_gps.show() self.sb_prog = gtk.ProgressBar() self.sb_prog.set_size_request(150, -1) self.sb_prog.show() hbox.pack_start(self.sb_coords, 1,1,1) hbox.pack_start(self.sb_center, 1,1,1) hbox.pack_start(self.sb_prog, 0,0,0) hbox.pack_start(self.sb_gps, 1,1,1) hbox.show() box.pack_start(self.sw, 1,1,1) box.pack_start(self.make_bottom_pane(), 0,0,0) box.pack_start(hbox, 0,0,0) box.show() self.set_default_size(600,600) self.set_geometry_hints(max_width=tiles*256, max_height=tiles*256) self.markers = {} self.colors = {} self.color_index = 0 self.add(box) self.connect("destroy", self.ev_destroy) self.connect("delete_event", self.ev_delete) self._popup_items = {} self.add_popup_handler(_("Center here"), lambda a, vals: self.recenter(vals["lat"], vals["lon"])) def set_mark_at(a, vals): p = map_sources.MapStation("STATION", vals["lat"], vals["lon"]) p.set_icon_from_aprs_sym("\\<") point, group = self.prompt_to_set_marker(p) if not point: return for source in self.map_sources: print "%s,%s" % (source.get_name(), group) if source.get_name() == group: print "Adding new point %s to %s" % (point.get_name(), source.get_name()) source.add_point(point) source.save() return # No matching group q = "%s %s %s" % \ (_("Group"), group, _("does not exist. Do you want to create it?")) if not ask_for_confirmation(q): return s = map_sources.MapFileSource.open_source_by_name(self.config, group, True) s.add_point(point) s.save() self.add_map_source(s) self.add_popup_handler(_("New marker here"), set_mark_at) self.add_popup_handler(_("Broadcast this location"), lambda a, vals: self.prompt_to_send_loc(vals["lat"], vals["lon"])) self.info_window = gtk.Window(gtk.WINDOW_POPUP) self.info_window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_MENU) self.info_window.set_decorated(False) self.info_window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("yellow")) def add_popup_handler(self, name, handler): self._popup_items[name] = handler def set_zoom(self, zoom): self.map.set_zoom(zoom) def set_center(self, lat, lon): self.map.set_center(lat, lon) if __name__ == "__main__": import sys import gps if len(sys.argv) == 3: m = MapWindow() m.set_center(gps.parse_dms(sys.argv[1]), gps.parse_dms(sys.argv[2])) m.set_zoom(15) else: m = MapWindow() m.set_center(45.525012, -122.916434) m.set_zoom(14) m.set_marker(GPSPosition(station="KI4IFW_H", lat=45.520, lon=-122.916434)) m.set_marker(GPSPosition(station="KE7FTE", lat=45.5363, lon=-122.9105)) m.set_marker(GPSPosition(station="KA7VQH", lat=45.4846, lon=-122.8278)) m.set_marker(GPSPosition(station="N7QQU", lat=45.5625, lon=-122.8645)) m.del_marker("N7QQU") m.show() try: gtk.main() except: pass # area = gtk.DrawingArea() # area.set_size_request(768, 768) # # w = gtk.Window(gtk.WINDOW_TOPLEVEL) # w.add(area) # area.show() # w.show() # # def expose(area, event): # for i in range(1,4): # img = gtk.gdk.pixbuf_new_from_file("/tmp/tile%i.png" % i) # area.window.draw_pixbuf(area.get_style().black_gc, # img, # 0, 0, 256 * (i-1), 0, 256, 256) # # area.connect("expose-event", expose) # d-rats-0.3.3/d_rats/miscwidgets.py000066400000000000000000000547671160617671700171160ustar00rootroot00000000000000# # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject import pango import os import platform class KeyedListWidget(gtk.HBox): __gsignals__ = { "item-selected" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), "item-toggled" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_BOOLEAN, (gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)), "item-set" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), } def _toggle(self, rend, path, colnum): if self.__toggle_connected: self.__store[path][colnum] = not self.__store[path][colnum] iter = self.__store.get_iter(path) id, = self.__store.get(iter, 0) self.emit("item-toggled", id, self.__store[path][colnum]) def _edited(self, rend, path, new, colnum): iter = self.__store.get_iter(path) key = self.__store.get(iter, 0) self.__store.set(iter, colnum, new) self.emit("item-set", key) def _mouse(self, view, event): x, y = event.get_coords() path = self.__view.get_path_at_pos(int(x), int(y)) if path: self.__view.set_cursor_on_cell(path[0]) sel = self.get_selected() if sel: self.emit("item-selected", sel) def _make_view(self): colnum = -1 for typ, cap in self.columns: colnum += 1 if colnum == 0: continue # Key column if typ in [gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_FLOAT]: rend = gtk.CellRendererText() rend.set_property("ellipsize", pango.ELLIPSIZE_END) column = gtk.TreeViewColumn(cap, rend, text=colnum) elif typ in [gobject.TYPE_BOOLEAN]: rend = gtk.CellRendererToggle() rend.connect("toggled", self._toggle, colnum) column = gtk.TreeViewColumn(cap, rend, active=colnum) else: raise Exception("Unsupported type %s" % typ) column.set_sort_column_id(colnum) self.__view.append_column(column) self.__view.connect("button_press_event", self._mouse) def set_item(self, key, *values): iter = self.__store.get_iter_first() while iter: id, = self.__store.get(iter, 0) if id == key: self.__store.insert_after(iter, row=(id,)+values) self.__store.remove(iter) return iter = self.__store.iter_next(iter) self.__store.append(row=(key,) + values) self.emit("item-set", key) def get_item(self, key): iter = self.__store.get_iter_first() while iter: vals = self.__store.get(iter, *tuple(range(len(self.columns)))) if vals[0] == key: return vals iter = self.__store.iter_next(iter) return None def del_item(self, key): iter = self.__store.get_iter_first() while iter: id, = self.__store.get(iter, 0) if id == key: self.__store.remove(iter) return True iter = self.__store.iter_next(iter) return False def has_item(self, key): return self.get_item(key) is not None def get_selected(self): try: (store, iter) = self.__view.get_selection().get_selected() return store.get(iter, 0)[0] except Exception, e: print "Unable to find selected: %s" % e return None def select_item(self, key): if key is None: sel = self.__view.get_selection() sel.unselect_all() return True iter = self.__store.get_iter_first() while iter: if self.__store.get(iter, 0)[0] == key: selection = self.__view.get_selection() path = self.__store.get_path(iter) selection.select_path(path) return True iter = self.__store.iter_next(iter) return False def get_keys(self): keys = [] iter = self.__store.get_iter_first() while iter: key, = self.__store.get(iter, 0) keys.append(key) iter = self.__store.iter_next(iter) return keys def __init__(self, columns): gtk.HBox.__init__(self, True, 0) self.columns = columns types = tuple([x for x,y in columns]) self.__store = gtk.ListStore(*types) self.__view = gtk.TreeView(self.__store) self.pack_start(self.__view, 1, 1, 1) self.__toggle_connected = False self._make_view() self.__view.show() def connect(self, signame, *args): if signame == "item-toggled": self.__toggle_connected = True gtk.HBox.connect(self, signame, *args) def set_editable(self, column, is_editable): col = self.__view.get_column(column) rend = col.get_cell_renderers()[0] rend.set_property("editable", True) rend.connect("edited", self._edited, column + 1) def set_sort_column(self, column, value=None): self.__view.get_model().set_sort_column_id(column, gtk.SORT_ASCENDING) def set_resizable(self, column, resizable, ellipsize=False): col = self.__view.get_column(column) col.set_resizable(resizable) rend = col.get_cell_renderers()[0] rend.set_property("ellipsize", ellipsize and pango.ELLIPSIZE_END \ or pango.ELLIPSIZE_NONE) def set_expander(self, column): col = self.__view.get_column(column) self.__view.set_expander_column(col) def set_password(self, column): def render_password(foo, rend, model, iter): val = model.get(iter, column+1)[0] rend.set_property("text", "*" * len(val)); col = self.__view.get_column(column) rnd = col.get_cell_renderers()[0] col.set_cell_data_func(rnd, render_password) class ListWidget(gtk.HBox): __gsignals__ = { "click-on-list" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.TreeView, gtk.gdk.Event)), "item-toggled" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), } store_type = gtk.ListStore def mouse_cb(self, view, event): self.emit("click-on-list", view, event) # pylint: disable-msg=W0613 def _toggle(self, render, path, column): self._store[path][column] = not self._store[path][column] iter = self._store.get_iter(path) vals = tuple(self._store.get(iter, *tuple(range(self._ncols)))) for cb in self.toggle_cb: cb(*vals) self.emit("item-toggled", vals) def make_view(self, columns): self._view = gtk.TreeView(self._store) for _type, _col in columns: if _col.startswith("__"): continue index = columns.index((_type, _col)) if _type == gobject.TYPE_STRING or \ _type == gobject.TYPE_INT or \ _type == gobject.TYPE_FLOAT: rend = gtk.CellRendererText() column = gtk.TreeViewColumn(_col, rend, text=index) column.set_resizable(True) rend.set_property("ellipsize", pango.ELLIPSIZE_END) elif _type == gobject.TYPE_BOOLEAN: rend = gtk.CellRendererToggle() rend.connect("toggled", self._toggle, index) column = gtk.TreeViewColumn(_col, rend, active=index) else: raise Exception("Unknown column type (%i)" % index) column.set_sort_column_id(index) self._view.append_column(column) self._view.connect("button_press_event", self.mouse_cb) def __init__(self, columns, parent=True): gtk.HBox.__init__(self) # pylint: disable-msg=W0612 col_types = tuple([x for x, y in columns]) self._ncols = len(col_types) self._store = self.store_type(*col_types) self._view = None self.make_view(columns) self._view.show() if parent: self.pack_start(self._view, 1, 1, 1) self.toggle_cb = [] def packable(self): return self._view def add_item(self, *vals): if len(vals) != self._ncols: raise Exception("Need %i columns" % self._ncols) args = [] i = 0 for val in vals: args.append(i) args.append(val) i += 1 args = tuple(args) iter = self._store.append() self._store.set(iter, *args) def _remove_item(self, model, path, iter, match): vals = model.get(iter, *tuple(range(0, self._ncols))) if vals == match: model.remove(iter) def remove_item(self, *vals): if len(vals) != self._ncols: raise Exception("Need %i columns" % self._ncols) def remove_selected(self): try: (lst, iter) = self._view.get_selection().get_selected() lst.remove(iter) except Exception, e: print "Unable to remove selected: %s" % e def get_selected(self, take_default=False): (lst, iter) = self._view.get_selection().get_selected() if not iter and take_default: iter = lst.get_iter_first() return lst.get(iter, *tuple(range(self._ncols))) def move_selected(self, delta): (lst, iter) = self._view.get_selection().get_selected() pos = int(lst.get_path(iter)[0]) try: target = None if delta > 0 and pos > 0: target = lst.get_iter(pos-1) elif delta < 0: target = lst.get_iter(pos+1) except Exception, e: return False if target: return lst.swap(iter, target) def _get_value(self, model, path, iter, lst): lst.append(model.get(iter, *tuple(range(0, self._ncols)))) def get_values(self): lst = [] self._store.foreach(self._get_value, lst) return lst def set_values(self, lst): self._store.clear() for i in lst: self.add_item(*i) class TreeWidget(ListWidget): store_type = gtk.TreeStore # pylint: disable-msg=W0613 def _toggle(self, render, path, column): self._store[path][column] = not self._store[path][column] iter = self._store.get_iter(path) vals = tuple(self._store.get(iter, *tuple(range(self._ncols)))) piter = self._store.iter_parent(iter) if piter: parent = self._store.get(piter, self._key)[0] else: parent = None for cb in self.toggle_cb: cb(parent, *vals) def __init__(self, columns, key, parent=True): ListWidget.__init__(self, columns, parent) self._key = key def _add_item(self, piter, *vals): args = [] i = 0 for val in vals: args.append(i) args.append(val) i += 1 args = tuple(args) iter = self._store.append(piter) self._store.set(iter, *args) def _iter_of(self, key, iter=None): if not iter: iter = self._store.get_iter_first() while iter is not None: _id = self._store.get(iter, self._key)[0] if _id == key: return iter iter = self._store.iter_next(iter) return None def add_item(self, parent, *vals): if len(vals) != self._ncols: raise Exception("Need %i columns" % self._ncols) if not parent: self._add_item(None, *vals) else: iter = self._iter_of(parent) if iter: self._add_item(iter, *vals) else: raise Exception("Parent not found: %s", parent) def _set_values(self, parent, vals): if isinstance(vals, dict): for key, val in vals.items(): iter = self._store.append(parent) self._store.set(iter, self._key, key) self._set_values(iter, val) elif isinstance(vals, list): for i in vals: self._set_values(parent, i) elif isinstance(vals, tuple): self._add_item(parent, *vals) else: print "Unknown type: %s" % vals def set_values(self, vals): self._store.clear() self._set_values(self._store.get_iter_first(), vals) def _get_values(self, iter, onlyone=False): l = [] while iter: if self._store.iter_has_child(iter): l.append((self._store.get(iter, *tuple(range(self._ncols))), self._get_values(self._store.iter_children(iter)))) else: l.append(self._store.get(iter, *tuple(range(self._ncols)))) if onlyone: break iter = self._store.iter_next(iter) return l def get_values(self, parent=None): if parent: iter = self._iter_of(parent) else: iter = self._store.get_iter_first() return self._get_values(iter, parent is not None) def clear(self): self._store.clear() def del_item(self, parent, key): iter = self._iter_of(key, self._store.iter_children(self._iter_of(parent))) if iter: self._store.remove(iter) else: raise Exception("Item not found") def get_item(self, parent, key): iter = self._iter_of(key, self._store.iter_children(self._iter_of(parent))) if iter: return self._store.get(iter, *(tuple(range(0, self._ncols)))) else: raise Exception("Item not found") def set_item(self, parent, *vals): iter = self._iter_of(vals[self._key], self._store.iter_children(self._iter_of(parent))) if iter: args = [] i = 0 for val in vals: args.append(i) args.append(val) i += 1 self._store.set(iter, *(tuple(args))) else: raise Exception("Item not found") class ProgressDialog(gtk.Window): def __init__(self, title, parent=None): gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_modal(True) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.set_title(title) if parent: self.set_transient_for(parent) self.set_resizable(False) vbox = gtk.VBox(False, 2) self.label = gtk.Label("") self.label.set_size_request(100, 50) self.label.show() self.pbar = gtk.ProgressBar() self.pbar.show() vbox.pack_start(self.label, 0, 0, 0) vbox.pack_start(self.pbar, 0, 0, 0) vbox.show() self.add(vbox) def set_text(self, text): self.label.set_text(text) self.queue_draw() while gtk.events_pending(): gtk.main_iteration_do(False) def set_fraction(self, frac): self.pbar.set_fraction(frac) self.queue_draw() while gtk.events_pending(): gtk.main_iteration_do(False) class LatLonEntry(gtk.Entry): def __init__(self, *args): gtk.Entry.__init__(self, *args) self.connect("changed", self.format) def format(self, entry): string = entry.get_text() if string is None: return deg = u"\u00b0" while " " in string: if "." in string: break elif deg not in string: string = string.replace(" ", deg) elif "'" not in string: string = string.replace(" ", "'") elif '"' not in string: string = string.replace(" ", '"') else: string = string.replace(" ", "") entry.set_text(string) def parse_dd(self, string): return float(string) def parse_dm(self, string): string = string.strip() string = string.replace(' ', ' ') (_degrees, _minutes) = string.split(' ', 2) degrees = int(_degrees) minutes = float(_minutes) return degrees + (minutes / 60.0) def parse_dms(self, string): string = string.replace(u"\u00b0", " ") string = string.replace('"', ' ') string = string.replace("'", ' ') string = string.replace(' ', ' ') string = string.strip() items = string.split(' ') if len(items) > 3: raise Exception("Invalid format") elif len(items) == 3: deg = items[0] mns = items[1] sec = items[2] elif len(items) == 2: deg = items[0] mns = items[1] sec = 0 elif len(items) == 1: deg = items[0] mns = 0 sec = 0 else: deg = 0 mns = 0 sec = 0 degrees = int(deg) minutes = int(mns) seconds = float(sec) return degrees + (minutes / 60.0) + (seconds / 3600.0) def value(self): string = self.get_text() try: return self.parse_dd(string) except: try: return self.parse_dm(string) except: try: return self.parse_dms(string) except Exception, e: print "DMS: %s" % e raise Exception("Invalid format") def validate(self): try: self.value() return True except: return False class YesNoDialog(gtk.Dialog): def __init__(self, title="", parent=None, buttons=None): gtk.Dialog.__init__(self, title=title, parent=parent, buttons=buttons) self._label = gtk.Label("") self._label.show() # pylint: disable-msg=E1101 self.vbox.pack_start(self._label, 1, 1, 1) def set_text(self, text): self._label.set_text(text) def make_choice(options, editable=True, default=None): if editable: sel = gtk.combo_box_entry_new_text() else: sel = gtk.combo_box_new_text() for opt in options: sel.append_text(opt) if default: try: idx = options.index(default) sel.set_active(idx) except: pass return sel class FilenameBox(gtk.HBox): __gsignals__ = { "filename-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } def do_browse(self, _, dir): if self.filename.get_text(): start = os.path.dirname(self.filename.get_text()) else: start = None if dir: fn = platform.get_platform().gui_select_dir(start) else: fn = platform.get_platform().gui_save_file(start) if fn: self.filename.set_text(fn) def do_changed(self, _): self.emit("filename_changed") def __init__(self, find_dir=False, types=[]): gtk.HBox.__init__(self, False, 0) self.types = types self.filename = gtk.Entry() self.filename.show() self.pack_start(self.filename, 1, 1, 1) browse = gtk.Button("...") browse.show() self.pack_start(browse, 0, 0, 0) self.filename.connect("changed", self.do_changed) browse.connect("clicked", self.do_browse, find_dir) def set_filename(self, fn): self.filename.set_text(fn) def get_filename(self): return self.filename.get_text() def set_mutable(self, mutable): self.filename.set_sensitive(mutable) def make_pixbuf_choice(options, default=None): store = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) box = gtk.ComboBox(store) cell = gtk.CellRendererPixbuf() box.pack_start(cell, True) box.add_attribute(cell, "pixbuf", 0) cell = gtk.CellRendererText() box.pack_start(cell, True) box.add_attribute(cell, "text", 1) _default = None for pic, value in options: iter = store.append() store.set(iter, 0, pic, 1, value) if default == value: _default = options.index((pic, value)) if _default: box.set_active(_default) return box def test(): win = gtk.Window(gtk.WINDOW_TOPLEVEL) lst = ListWidget([(gobject.TYPE_STRING, "Foo"), (gobject.TYPE_BOOLEAN, "Bar")]) lst.add_item("Test1", True) lst.set_values([("Test2", True), ("Test3", False)]) lst.show() win.add(lst) win.show() win1 = ProgressDialog("foo") win1.show() win2 = gtk.Window(gtk.WINDOW_TOPLEVEL) lle = LatLonEntry() lle.show() win2.add(lle) win2.show() win3 = gtk.Window(gtk.WINDOW_TOPLEVEL) lst = TreeWidget([(gobject.TYPE_STRING, "Id"), (gobject.TYPE_STRING, "Value")], 1) #l.add_item(None, "Foo", "Bar") #l.add_item("Foo", "Bar", "Baz") lst.set_values({"Fruit" : [("Apple", "Red"), ("Orange", "Orange")], "Pizza" : [("Cheese", "Simple"), ("Pepperoni", "Yummy")]}) lst.add_item("Fruit", "Bananna", "Yellow") lst.show() win3.add(lst) win3.show() def print_val(entry): if entry.validate(): print "Valid: %s" % entry.value() else: print "Invalid" lle.connect("activate", print_val) lle.set_text("45 13 12") try: gtk.main() except KeyboardInterrupt: pass print lst.get_values() if __name__ == "__main__": test() d-rats-0.3.3/d_rats/msgrouting.py000066400000000000000000000437571160617671700167670ustar00rootroot00000000000000# # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import threading import time import os import smtplib import shutil from glob import glob try: from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.message import Message except ImportError: # Python 2.4 from email import MIMEMultipart from email import MIMEBase from email import MIMEText from email import Message import gobject import formgui import signals import emailgw import utils import wl2k import mainapp import traceback CALL_TIMEOUT_RETRY = 300 MSG_LOCK_LOCK = threading.Lock() def __msg_lockfile(fn): name = os.path.basename(fn) path = os.path.dirname(fn) return os.path.join(path, ".lock.%s" % name) def msg_is_locked(fn): return os.path.exists(__msg_lockfile(fn)) def msg_lock(fn): MSG_LOCK_LOCK.acquire() if not msg_is_locked(fn): lf = file(__msg_lockfile(fn), "w") traceback.print_stack(file=lf) lf.close() success = True else: lf = file(__msg_lockfile(fn), "r") print "------ LOCK OWNED BY -------\n%s------------\n" % lf.read() lf.close() success = False MSG_LOCK_LOCK.release() #print "Locked %s: %s" % (fn, success) return success def msg_unlock(fn): success = True MSG_LOCK_LOCK.acquire() try: os.remove(__msg_lockfile(fn)) except OSError: utils.log_exception() success = False MSG_LOCK_LOCK.release() #print "Unlocked %s: %s" % (fn, success) return success def gratuitous_next_hop(route, path): path_nodes = path[1:] route_nodes = route.split(";") if len(path_nodes) >= len(route_nodes): print "Nothing left in the routes" return None for i in range(0, len(path_nodes)): if path_nodes[i] != route_nodes[i]: print "Path element %i (%s) does not match route %s" % \ (i, path_nodes[i], route_nodes[i]) return None return route_nodes[len(path_nodes)] def is_sendable_dest(mycall, string): # Specifically for me if string == mycall: print "for me" return False # Empty string if not string.strip(): print "empty: %s %s" % (string, string.strip()) return False # Is an email address: if "@" in string: print "is an Email" return True # Contains lowercase characters if string != string.upper(): print "lowercase" return False # Contains spaces if string != string.split()[0]: print "spaces" return False # Contains a gratuitous route and we're the last in line if ";" in string and string.split(";")[-1] == mycall: print "End of grat" return False print "default to call" # Looks like it's a candidate to be routed return True def form_to_email(config, msgfn, replyto=None): form = formgui.FormFile(msgfn) form.configure(config) if not replyto: replyto = "DO_NOT_REPLY@d-rats.com" sender = form.get_path_src() for i in "<>\"'": if i in sender: sender = sender.replace(i, "") root = MIMEMultipart("related") root["Subject"] = form.get_subject_string() root["From"] = '"%s" <%s>' % (sender, replyto) root["To"] = form.get_path_dst() root["Reply-To"] = root["From"] root["Message-id"] = form.get_path_mid() root.preamble = "This is a multi-part message in MIME format" altp = MIMEMultipart("alternative") root.attach(altp) warn = "This message is a D-RATS rich form and is only viewable " +\ "in D_RATS itself or in an HTML-capable email client" if form.id == "email": altp.attach(MIMEText(form.get_field_value("message"), "plain")) else: altp.attach(MIMEText(warn, "plain")) altp.attach(MIMEText(form.export_to_string(), "html")) payload = MIMEBase("d-rats", "form_xml") payload.set_payload(form.get_xml()) payload.add_header("Content-Disposition", "attachment", filename="do_not_open_me") root.attach(payload) del form return root def move_to_folder(config, msg, folder): newfn = os.path.join(config.form_store_dir(), folder, os.path.basename(msg)) msg_lock(newfn) shutil.move(msg, newfn) msg_unlock(newfn) def move_to_outgoing(config, msg): return move_to_folder(config, msg, "Outbox") class MessageRoute(object): def __init__(self, line): self.dest, self.gw, self.port = line.split() class MessageRouter(gobject.GObject): __gsignals__ = { "get-station-list" : signals.GET_STATION_LIST, "user-send-form" : signals.USER_SEND_FORM, "form-sent" : signals.FORM_SENT, "form-received" : signals.FORM_RECEIVED, "ping-station" : signals.PING_STATION, "event" : signals.EVENT, } _signals = __gsignals__ def _emit(self, signal, *args): gobject.idle_add(self.emit, signal, *args) def __proxy_emit(self, signal): def handler(obj, *args): print "Proxy emit %s: %s" % (signal, args) self._emit(signal, *args) return handler def __init__(self, config): gobject.GObject.__init__(self) self.__event = threading.Event() self.__config = config self.__sent_call = {} self.__sent_port = {} self.__file_to_call = {} self.__failed_stations = {} self.__pinged_stations = {} self.__thread = None self.__enabled = False def _get_routes(self): rf = self.__config.platform.config_file("routes.txt") try: f = file(rf) lines = f.readlines() f.close() except IOError: return {} routes = {} for line in lines: if not line.strip() or line.startswith("#"): continue try: dest, gw, port = line.split() routes[dest] = gw except Exception, e: print "Error parsing line '%s': %s" % (line, e) return routes def _sleep(self): t = self.__config.getint("settings", "msg_flush") time.sleep(t) def _p(self, string): print "[MR] %s" % string import sys sys.stdout.flush() def _get_queue(self): queue = {} qd = os.path.join(self.__config.form_store_dir(), "Outbox") fl = glob(os.path.join(qd, "*.xml")) for f in fl: if not msg_lock(f): print "Message %s is locked, skipping" % f continue form = formgui.FormFile(f) call = form.get_path_dst() del form if not call: msg_unlock(f) continue elif not queue.has_key(call): queue[call] = [f] else: queue[call].append(f) return queue def _send_form(self, call, port, filename): self.__sent_call[call] = time.time() self.__sent_port[port] = time.time() self.__file_to_call[filename] = call self._emit("user-send-form", call, port, filename, "Foo") def _sent_recently(self, call): if self.__sent_call.has_key(call): return (time.time() - self.__sent_call[call]) < CALL_TIMEOUT_RETRY return False def _port_free(self, port): return not self.__sent_port.has_key(port) def _route_msg(self, src, dst, path, slist, routes): invalid = [] def old(call): station = slist.get(call, None) if not station: return True ttl = self.__config.getint("settings", "station_msg_ttl") return (time.time() - station.get_heard()) > ttl while True: # Choose the @route for @dst if ";" in dst: # Gratuitous routing takes precedence route = gratuitous_next_hop(dst, path) print "Route for %s: %s (%s)" % (dst, route, path) break elif "@" in dst and dst not in invalid and \ not ":" in dst and \ emailgw.validate_incoming(self.__config, src, dst): # Out via email route = dst elif slist.has_key(dst) and dst not in invalid: # Direct send route = dst elif routes.has_key(dst) and routes[dst] not in invalid: # Static route present route = routes[dst] elif routes.has_key("*") and routes["*"] not in invalid: # Default route route = routes["*"] elif dst.upper().startswith("WL2K:"): route = dst else: route = None break # Validate the @route if route.upper().startswith("WL2K:"): break # WL2K is easy elif route != dst and route in path: print "Route %s in path" % route invalid.append(route) route = None # Don't route to the same location twice elif self._is_station_failed(route): print "Route %s is failed" % route invalid.append(route) route = None # This one is not responding lately elif old(route) and self._station_pinged_out(route): print "Route %s for %s is pinged out" % (route, dst) invalid.append(route) route = None # This one has been pinged and isn't responding else: break # We have a route to try if not route: self._p("No route for station %s" % dst) elif old(route) and "@" not in route and ":" not in route: # This station is heard, but a long time ago. Ping it first # and consider it unrouteable for now route_station = slist.get(route, None) self._station_pinged_incr(route) if route_station: self._p("Pinging stale route %s" % route) self._emit("ping-station", route, route_station.get_port()) route = None else : self._station_pinged_clear(route) self._p("Routing message for %s to %s" % (dst, route)) return route def _form_to_wl2k_em(self, dst, msgfn): form = formgui.FormFile(msgfn) if form.id != "email": raise Exception("WL2K support requires email type message") payload = form.get_field_value("message") call = self.__config.get("user", "callsign") call = "".join([x for x in call if x.isalpha()]) mid = time.strftime("D%H%M%S") + call[:12] msg = "Mid: %s\r\n" % mid + \ "Subject: %s\r\n" % form.get_subject_string() + \ "From: %s\r\n" % form.get_path_src() + \ "To: %s\r\n" % dst + \ "Body: %i\r\n" % len(payload) + \ "Date: %s\r\n" % time.strftime("%Y/%m/%d %H:%M", time.gmtime()) + \ "\r\n" + \ payload + "\r\n" + \ "\r\n" return msg def _route_via_email(self, call, msgfn): server = self.__config.get("settings", "smtp_server") replyto = self.__config.get("settings", "smtp_replyto") tls = self.__config.getboolean("settings", "smtp_tls") user = self.__config.get("settings", "smtp_username") pwrd = self.__config.get("settings", "smtp_password") port = self.__config.getint("settings", "smtp_port") msg = form_to_email(self.__config, msgfn, replyto) mailer = smtplib.SMTP(server, port) mailer.set_debuglevel(1) mailer.ehlo() if tls: mailer.starttls() mailer.ehlo() if user and pwrd: mailer.login(user, pwrd) mailer.sendmail(replyto, msg["To"], msg.as_string()) mailer.quit() self._emit("form-sent", -1, msgfn) return True def _route_via_station(self, call, route, slist, msg): if self._sent_recently(route): self._p("Call %s is busy" % route) return False print slist port = slist[route].get_port() if not self._port_free(port): self._p("I think port %s is busy" % port) return False # likely already a transfer going here so skip it self._p("Sending %s to %s (via %s)" % (msg, call, route)) self._send_form(route, port, msg) return True def _route_via_wl2k(self, src, dst, msgfn): foo, addr = dst.split(":") msg = self._form_to_wl2k_em(addr, msgfn) def complete(thread, status, error): if status: self._emit("form-sent", -1, msgfn) else: print "Failed to send via WL2K: %s" % error mt = wl2k.wl2k_auto_thread(mainapp.get_mainapp(), src, send_msgs=[msg]) mt.connect("mail-thread-complete", complete) mt.connect("event", self.__proxy_emit("event")) mt.connect("form-received", self.__proxy_emit("form-received")) mt.connect("form-sent", self.__proxy_emit("form-sent")) mt.start() return True def _route_message(self, msg, slist, routes): form = formgui.FormFile(msg) path = form.get_path() emok = path[-2:] != ["EMAIL", self.__config.get("user", "callsign")] src = form.get_path_src() dst = form.get_path_dst() del form routed = False route = self._route_msg(src, dst, path, slist, routes) if not route: pass elif route.upper().startswith("WL2K:"): routed = self._route_via_wl2k(src, route, msg) elif "@" in src and "@" in dst: # Don't route a message from email to email pass elif "@" in route: if emok: routed = self._route_via_email(dst, msg) else: routed = self._route_via_station(dst, route, slist, msg) return routed def _run_one(self, queue): plist = self.emit("get-station-list") slist = {} routes = self._get_routes() for port, stations in plist.items(): for station in stations: slist[str(station)] = station for dst, callq in queue.items(): for msg in callq: try: routed = self._route_message(msg, slist, routes) except Exception: utils.log_exception() routed = False if not routed: msg_unlock(msg) def _run(self): while self.__enabled: if self.__config.getboolean("settings", "msg_forward") or \ self.__event.isSet(): print "Running routing loop" queue = self._get_queue() try: self._run_one(queue) except Exception, e: utils.log_exception() print "Fail-safe unlocking messages in queue:" for msgs in queue.values(): for msg in msgs: print "Unlocking %s" % msg msg_unlock(msg) self.__event.clear() self.__event.wait(self.__config.getint("settings", "msg_flush")) def trigger(self): if not self.__thread.isAlive(): self.start() else: self.__event.set() def start(self): self._p("Starting message router thread") self.__enabled = True self.__event.clear() self.__thread = threading.Thread(target=self._run) self.__thread.setDaemon(True) self.__thread.start() def stop(self): self.__enabled = False self.__thread.join() def _update_path(self, fn, call): form = formgui.FormFile(fn) form.add_path_element(call) form.save_to(fn) def _station_succeeded(self, call): self.__failed_stations[call] = 0 def _station_failed(self, call): self.__failed_stations[call] = self.__failed_stations.get(call, 0) + 1 print "Fail count for %s is %i" % (call, self.__failed_stations[call]) def _is_station_failed(self, call): return self.__failed_stations.get(call, 0) >= 3 def _station_pinged_incr(self, call): self.__pinged_stations[call] = self.__pinged_stations.get(call, 0) + 1 return self.__pinged_stations[call] def _station_pinged_clear(self, call): self.__pinged_stations[call] = 0 def _station_pinged_out(self, call): return self.__pinged_stations.get(call, 0) >= 3 def form_xfer_done(self, fn, port, failed): self._p("File %s on %s done" % (fn, port)) if fn and msg_is_locked(fn): msg_unlock(fn) call = self.__file_to_call.get(fn, None) if call and self.__sent_call.has_key(call): # This callsign completed (or failed) a transfer if failed: self._station_failed(call) else: self._station_succeeded(call) self._update_path(fn, call) del self.__sent_call[call] del self.__file_to_call[fn] if self.__sent_port.has_key(port): # This port is now open for another transfer del self.__sent_port[port] d-rats-0.3.3/d_rats/platform.py000066400000000000000000000314221160617671700163770ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import glob import commands import subprocess import urllib def find_me(): return sys.modules["d_rats.platform"].__file__ class Platform(object): # pylint: disable-msg=R0201 def __init__(self, basepath): self._base = basepath self._source_dir = os.path.abspath(".") self._connected = True def __str__(self): l = ["Platform %s:" % str(self.__class__.__name__)] l.append(" base: %s" % self.config_dir()) l.append(" source_dir: %s" % self.source_dir()) l.append(" OS version: %s" % self.os_version_string()) return os.linesep.join(l) def config_dir(self): return self._base def source_dir(self): return self._source_dir def log_dir(self): logdir = os.path.join(self.config_dir(), "logs") if not os.path.isdir(logdir): os.mkdir(logdir) return logdir def filter_filename(self, filename): return filename def log_file(self, filename): filename = self.filter_filename(filename + ".txt").replace(" ", "_") return os.path.join(self.log_dir(), filename) def config_file(self, filename): return os.path.join(self.config_dir(), self.filter_filename(filename)) def open_text_file(self, path): raise NotImplementedError("The base class can't do that") def open_html_file(self, path): raise NotImplementedError("The base class can't do that") def list_serial_ports(self): return [] def default_dir(self): return "." def gui_open_file(self, start_dir=None): import gtk dlg = gtk.FileChooserDialog("Select a file to open", None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) if start_dir and os.path.isdir(start_dir): dlg.set_current_folder(start_dir) res = dlg.run() fname = dlg.get_filename() dlg.destroy() if res == gtk.RESPONSE_OK: return fname else: return None def gui_save_file(self, start_dir=None, default_name=None): import gtk dlg = gtk.FileChooserDialog("Save file as", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) if start_dir and os.path.isdir(start_dir): dlg.set_current_folder(start_dir) if default_name: dlg.set_current_name(default_name) res = dlg.run() fname = dlg.get_filename() dlg.destroy() if res == gtk.RESPONSE_OK: return fname else: return None def gui_select_dir(self, start_dir=None): import gtk dlg = gtk.FileChooserDialog("Choose folder", None, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) if start_dir and os.path.isdir(start_dir): dlg.set_current_folder(start_dir) res = dlg.run() fname = dlg.get_filename() dlg.destroy() if res == gtk.RESPONSE_OK and os.path.isdir(fname): return fname else: return None def os_version_string(self): return "Unknown Operating System" def run_sync(self, command): pipe = subprocess.Popen(command, stdout=subprocess.PIPE) data = pipe.stdout.read() return 0, data def retrieve_url(self, url): if self._connected: return urllib.urlretrieve(url) raise Exception("Not connected") def set_connected(self, connected): self._connected = connected def play_sound(self, soundfile): print "Sound is unsupported on this platform!" class UnixPlatform(Platform): def __init__(self, basepath): if not basepath: basepath = os.path.abspath(os.path.join(self.default_dir(), ".d-rats")) if not os.path.isdir(basepath): os.mkdir(basepath) Platform.__init__(self, basepath) def source_dir(self): if "site-packages" in find_me(): return "/usr/share/d-rats" elif "dist-packages" in find_me(): return "/usr/share/d-rats" elif "/usr/share/d-rats" in find_me(): return "/usr/share/d-rats" else: return self._source_dir def default_dir(self): return os.path.abspath(os.getenv("HOME")) def filter_filename(self, filename): return filename.replace("/", "") def _unix_doublefork_run(self, *args): pid1 = os.fork() if pid1 == 0: pid2 = os.fork() if pid2 == 0: print "Exec'ing %s" % str(args) os.execlp(args[0], *args) else: sys.exit(0) else: os.waitpid(pid1, 0) print "Exec child exited" def open_text_file(self, path): self._unix_doublefork_run("gedit", path) def open_html_file(self, path): self._unix_doublefork_run("firefox", path) def list_serial_ports(self): return sorted(glob.glob("/dev/ttyS*") + glob.glob("/dev/ttyUSB*")) def os_version_string(self): # pylint: disable-msg=W0703 try: issue = file("/etc/issue.net", "r") ver = issue.read().strip() issue.close() ver = "%s - %s" % (os.uname()[0], ver) except Exception: ver = " ".join(os.uname()) return ver def run_sync(self, command): return commands.getstatusoutput(command) def play_sound(self, soundfile): import ossaudiodev import sndhdr try: (t, r, c, f, b) = sndhdr.what(soundfile) except Exception, e: print "Unable to determine sound header of %s: %s" % (soundfile, e) return if t != "wav": print "Unable to play non-wav file %s" % soundfile return if b != 16: print "Unable to support strange non-16-bit audio (%i)" % b return dev = None try: dev = ossaudiodev.open("w") dev.setfmt(ossaudiodev.AFMT_S16_LE) dev.channels(c) dev.speed(r) f = file(soundfile, "rb") dev.write(f.read()) f.close() dev.close() except Exception, e: print "Error playing sound %s: %s" % (soundfile, e) if dev: dev.close() class MacOSXPlatform(UnixPlatform): def __init__(self, basepath): # We need to make sure DISPLAY is set if not os.environ.has_key("DISPLAY"): print "Forcing DISPLAY for MacOS" os.environ["DISPLAY"] = ":0" os.environ["PANGO_RC_FILE"] = "../Resources/etc/pango/pangorc" UnixPlatform.__init__(self, basepath) def open_html_file(self, path): self._unix_doublefork_run("open", path) def open_text_file(self, path): macos_textedit = "/Applications/TextEdit.app/Contents/MacOS/TextEdit" self._unix_doublefork_run(macos_textedit, path) def list_serial_ports(self): keyspan = glob.glob("/dev/cu.KeySerial*") prolific = glob.glob("/dev/tty.usbserial*") return sorted(keyspan + prolific) def os_version_string(self): return "MacOS X" def source_dir(self): if "site-packages" in find_me(): return "../Resources" else: return self._source_dir class Win32Platform(Platform): def __init__(self, basepath=None): if not basepath: appdata = os.getenv("APPDATA") if not appdata: appdata = "C:\\" basepath = os.path.abspath(os.path.join(appdata, "D-RATS")) if not os.path.isdir(basepath): os.mkdir(basepath) Platform.__init__(self, basepath) def default_dir(self): return os.path.abspath(os.path.join(os.getenv("USERPROFILE"), "Desktop")) def filter_filename(self, filename): for char in "/\\:*?\"<>|": filename = filename.replace(char, "") return filename def open_text_file(self, path): subprocess.Popen(["notepad", path]) return def open_html_file(self, path): subprocess.Popen(["explorer", path]) def list_serial_ports(self): import win32file import win32con ports = [] for i in range(1, 257): try: portname = "COM%i" % i mode = win32con.GENERIC_READ | win32con.GENERIC_WRITE port = \ win32file.CreateFile(portname, mode, win32con.FILE_SHARE_READ, None, win32con.OPEN_EXISTING, 0, None) ports.append(portname) win32file.CloseHandle(port) port = None except Exception: pass return ports def gui_open_file(self, start_dir=None): # pylint: disable-msg=W0703,W0613 import win32gui try: fname, _, _ = win32gui.GetOpenFileNameW() except Exception, e: print "Failed to get filename: %s" % e return None return str(fname) def gui_save_file(self, start_dir=None, default_name=None): # pylint: disable-msg=W0703,W0613 import win32gui try: fname, _, _ = win32gui.GetSaveFileNameW(File=default_name) except Exception, e: print "Failed to get filename: %s" % e return None return str(fname) def gui_select_dir(self, start_dir=None): # pylint: disable-msg=W0703,W0613 from win32com.shell import shell try: pidl, _, _ = shell.SHBrowseForFolder() fname = shell.SHGetPathFromIDList(pidl) except Exception, e: print "Failed to get directory: %s" % e return None return str(fname) def os_version_string(self): import win32api vers = { 4: "Windows 2000", 5: "Windows XP", 6: "Windows Vista", 7: "Windows 7", } (pform, _, build, _, _) = win32api.GetVersionEx() return vers.get(pform, "Win32 (Unknown %i:%i)" % (pform, build)) def play_sound(self, soundfile): import winsound winsound.PlaySound(soundfile, winsound.SND_FILENAME) def _get_platform(basepath): if os.name == "nt": return Win32Platform(basepath) elif sys.platform == "darwin": return MacOSXPlatform(basepath) else: return UnixPlatform(basepath) PLATFORM = None def get_platform(basepath=None): #pylint: disable-msg=W0602 global PLATFORM if not PLATFORM: PLATFORM = _get_platform(basepath) return PLATFORM if __name__ == "__main__": def do_test(): __pform = get_platform() print "Config dir: %s" % __pform.config_dir() print "Default dir: %s" % __pform.default_dir() print "Log file (foo): %s" % __pform.log_file("foo") print "Serial ports: %s" % __pform.list_serial_ports() print "OS Version: %s" % __pform.os_version_string() #__pform.open_text_file("d-rats.py") #print "Open file: %s" % __pform.gui_open_file() #print "Save file: %s" % __pform.gui_save_file(default_name="Foo.txt") #print "Open folder: %s" % __pform.gui_select_dir("/tmp") do_test() d-rats-0.3.3/d_rats/pluginsrv.py000066400000000000000000000141131160617671700166020ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import threading from SimpleXMLRPCServer import SimpleXMLRPCServer import gobject import signals import utils from d_rats.sessions import rpc class DRatsChatEvent(object): def __init__(self, src_station=None): self.__event = threading.Event() self.__src_station = src_station self.__text = None def get_src_station(self): return self.__src_station def set_chat_info(self, src, text): self.__src_station = src self.__text = text def get_text(self): return self.__text def set(self): self.__event.set() def wait(self, timeout): self.__event.wait(timeout) class DRatsPluginProxy(gobject.GObject): __gsignals__ = { "user-send-chat" : signals.USER_SEND_CHAT, "get-station-list" : signals.GET_STATION_LIST, "user-send-file" : signals.USER_SEND_FILE, "submit-rpc-job" : signals.SUBMIT_RPC_JOB, } _signals = __gsignals__ def __init__(self): gobject.GObject.__init__(self) self.__idcount = 0 self.__persist = {} self.__events = { "chat" : [], } def get_port(self, station): ports = self.emit("get-station-list") port = utils.port_for_station(ports, station) if not port: raise Exception("Station %s not heard" % station) return port def send_chat(self, port, message): """Send a chat @message on @port""" print "Sending chat on port %s: %s" % (port, message) self.emit("user-send-chat", "CQCQCQ", port, message, False) return 0 def list_ports(self): """Return a list of port names""" slist = self.emit("get-station-list") return slist.keys() def send_file(self, station, filename, port=None): """Send a file to @station specified by @filename on optional port. If @port is not specified, the last-heard port for @station will be used. An exception will be thrown if the last port cannot be determined""" if not port: port = self.get_port(station) sname = os.path.basename(filename) print "Sending file %s to %s on port %s" % (filename, station, port) self.emit("user-send-file", station, port, filename, sname) return 0 def submit_rpcjob(self, station, rpcname, port=None, params={}): """Submit an RPC job to @station of type @rpcname. Optionally specify the @port to be used. The @params structure is a key=value list of function(value) items to call on the job object before submission. Returns a job specifier to be used with get_result().""" if not rpcname.isalpha() or not rpcname.startswith("RPC"): raise Exception("Invalid RPC function call name") if not port: port = self.get_port(station) job = eval("rpcsession.%s('%s', 'New Job')" % (rpcname, station)) for key, val in params: func = job.__getattribute__(key) func(val) ident = self.__idcount self.__idcount += 1 def record_result(job, state, result, ident): self.__persist[ident] = result job.connect("state-change", record_result, ident) self.emit("submit-rpc-job", job, port) return ident def get_result(self, ident): """Get the result of job @ident. Returns a structure, empty until completion""" if self.__persist.has_key(ident): result = self.__persist[ident] del self.__persist[ident] else: result = {} return result def wait_for_chat(self, timeout, src_station=None): """Wait for a chat message for @timeout seconds. Optional filter @src_station avoids returning until a chat message from that station is received""" ev = DRatsChatEvent(src_station) self.__events["chat"].append(ev) ev.wait(timeout) if ev.get_text(): return ev.get_src_station(), ev.get_text() else: return "", "" def incoming_chat_message(self, src, dst, text): for ev in self.__events["chat"]: if not ev.get_src_station(): ev.set_chat_info(src, text) ev.set() elif ev.get_src_station() == src: ev.set_chat_info(src, text) ev.set() class DRatsPluginServer(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__(self, ("localhost", 9100)) self.__thread = None self.__proxy = DRatsPluginProxy() self.register_function(self.__proxy.send_chat, "send_chat") self.register_function(self.__proxy.list_ports, "list_ports") self.register_function(self.__proxy.send_file, "send_file") self.register_function(self.__proxy.submit_rpcjob, "submit_rpcjob") self.register_function(self.__proxy.get_result, "get_result") self.register_function(self.__proxy.wait_for_chat, "wait_for_chat") def serve_background(self): self.__thread = threading.Thread(target=self.serve_forever) self.__thread.setDaemon(True) self.__thread.start() print "Started serve_forever() thread" def incoming_chat_message(self, *args): self.__proxy.incoming_chat_message(*args) def get_proxy(self): return self.__proxy d-rats-0.3.3/d_rats/qst.py000066400000000000000000000506571160617671700153750ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import pygtk import gobject import time import datetime import copy import re import threading from commands import getstatusoutput as run from miscwidgets import make_choice, KeyedListWidget import miscwidgets import mainapp import platform import inputdialog import cap import wu import mapdisplay import gps from utils import NetFile, combo_select, get_icon try: import feedparser HAVE_FEEDPARSER = True except ImportError, e: print "FeedParser not available" HAVE_FEEDPARSER = False try: from hashlib import md5 except ImportError: print "Installing hashlib replacement hack" from utils import ExternalHash as md5 def do_dprs_calculator(initial=""): def ev_sym_changed(iconsel, oversel, icons): oversel.set_sensitive(icons[iconsel.get_active()][1][0] == "\\") d = inputdialog.FieldDialog(title=_("DPRS message")) msg = gtk.Entry(13) overlays = [chr(x) for x in range(ord(" "), ord("_"))] cur = initial if cur and cur[-3] == "*" and cur[3] == " ": msg.set_text(cur[4:-3]) dsym = cur[:2] deficn = gps.DPRS_TO_APRS.get(dsym, "/#") defovr = cur[2] if defovr not in overlays: print "Overlay %s not in list" % defovr defovr = " " else: deficn = "/#" defovr = " " icons = [] for sym in sorted(gps.DPRS_TO_APRS.values()): icon = get_icon(sym) if icon: icons.append((icon, sym)) iconsel = miscwidgets.make_pixbuf_choice(icons, deficn) oversel = miscwidgets.make_choice(overlays, False, defovr) iconsel.connect("changed", ev_sym_changed, oversel, icons) ev_sym_changed(iconsel, oversel, icons) d.add_field(_("Message"), msg) d.add_field(_("Icon"), iconsel) d.add_field(_("Overlay"), oversel) r = d.run() aicon = icons[iconsel.get_active()][1] mstr = msg.get_text().upper() over = oversel.get_active_text() d.destroy() if r != gtk.RESPONSE_OK: return dicon = gps.APRS_TO_DPRS[aicon] callsign = mainapp.get_mainapp().config.get("user", "callsign") string = "%s%s %s" % (dicon, over, mstr) check = gps.DPRS_checksum(callsign, string) return string + check class QSTText(gobject.GObject): __gsignals__ = { "qst-fired" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), } def __init__(self, config, content): gobject.GObject.__init__(self) self.config = config self.prefix = "[QST] " self.text = content self.raw = False def do_qst(self): return self.text def fire(self): val = self.do_qst() self.emit("qst-fired", self.prefix + val) class QSTExec(QSTText): def do_qst(self): size_limit = self.config.getint("settings", "qst_size_limit") pform = platform.get_platform() s, o = pform.run_sync(self.text) if s: print "Command failed with status %i" % s return o[:size_limit] class QSTFile(QSTText): def do_qst(self): size_limit = self.config.getint("settings", "qst_size_limit") try: f = NetFile(self.text) except: print "Unable to open file `%s'" % self.text return text = f.read() f.close() return text[:size_limit] class QSTGPS(QSTText): def __init__(self, config, content): QSTText.__init__(self, config, content) self.prefix = "" self.raw = True self.mainapp = mainapp.get_mainapp() self.fix = None def set_fix(self, fix): self.fix = fix def do_qst(self): if not self.fix: fix = self.mainapp.get_position() else: fix = self.fix fix.set_station(fix.station, self.text[:20]) if fix.valid: return fix.to_NMEA_GGA() else: return None class QSTGPSA(QSTGPS): def do_qst(self): if not self.fix: fix = self.mainapp.get_position() else: fix = self.fix if not "::" in self.text: fix.set_station(fix.station, self.text[:20]) if fix.valid: return fix.to_APRS(symtab=self.config.get("settings", "aprssymtab"), symbol=self.config.get("settings", "aprssymbol")) else: return None class QSTThreadedText(QSTText): def __init__(self, *a, **k): QSTText.__init__(self, *a, **k) self.thread = None def threaded_fire(self): msg = self.do_qst() self.thread = None if not msg: print "Skipping QST because no data was returned" return gobject.idle_add(self.emit, "qst-fired", "%s%s" % (self.prefix, msg)) def fire(self): if self.thread: print "QST thread still running, not starting another" return # This is a race, but probably pretty safe :) self.thread = threading.Thread(target=self.threaded_fire) self.thread.setDaemon(True) self.thread.start() print "Started a thread for QST data..." class QSTRSS(QSTThreadedText): def __init__(self, config, content): QSTThreadedText.__init__(self, config, content) self.last_id = "" def do_qst(self): rss = feedparser.parse(self.text) try: entry = rss.entries[-1] except IndexError: print "RSS feed had no entries" return None try: id = entry.id except AttributeError: # Not all feeds will have an id (I guess) id = md5(entry.description) if id != self.last_id: self.last_id = id text = str(entry.description) text = re.sub("<[^>]*?>", "", text) text = text[:8192] return text else: return None class QSTCAP(QSTThreadedText): def __init__(self, *args, **kwargs): QSTThreadedText.__init__(self, *args, **kwargs) self.last_date = None def determine_starting_item(self): cp = cap.CAPParserURL(self.text) if cp.events: lastev = cp.events[-1] delta = datetime.timedelta(seconds=1) self.last_date = (lastev.effective - delta) else: self.last_date = datetime.datetime.now() def do_qst(self): if self.last_date is None: self.determine_starting_item() print "Last date is %s" % self.last_date cp = cap.CAPParserURL(self.text) newev = cp.events_effective_after(self.last_date) if not newev: return None try: self.last_date = newev[-1].effective except IndexError: print "CAP feed had no entries" return None str = "" for i in newev: print "Sending CAP that is effective %s" % i.effective str += "\r\n-----\r\n%s\r\n-----\r\n" % i.report() return str class QSTWeatherWU(QSTThreadedText): pbase = "http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID=" abase = "http://api.wunderground.com/auto/wui/geo/WXCurrentObXML/index.xml?query=" def do_qst(self): obs = wu.WUObservation() try: t, s = self.text.split("/", 2) except Exception, e: print "Unable to split weather QST %s: %s" % (self.text, e) return None try: if t == _("Airport"): base = self.abase elif t == _("Personal"): base = self.pbase else: print "Unknown QSTWeatherWU type %s" % t return None print "Getting %s%s for %s/%s" % ( base, self.text, t, s) obs.from_uri(base + s) except Exception, e: print "Error getting weather: %s" % e return None return str(obs) class QSTStation(QSTGPSA): def get_source(self, name): import mainapp app = mainapp.get_mainapp() # Hack for this difficult case sources = app.map.get_map_sources() for source in sources: if source.get_name() == name: return source return None def get_station(self, source, station): for point in source.get_points(): if point.get_name() == station: return point return None def do_qst(self): try: (group, station) = self.text.split("::", 1) except Exception, e: print "QSTStation Error: %s" % e return None source = self.get_source(group) if source is None: print "Unknown group %s" % group return point = self.get_station(source, station) if point is None: print "Unknown station %s in group %s" % (station, group) return self.fix = gps.GPSPosition(point.get_latitude(), point.get_longitude(), point.get_name()) self.fix.set_station(self.fix.station, "VIA %s" % self.config.get("user", "callsign")) print "Sending position for %s/%s: %s" % (group, station, self.fix) return QSTGPSA.do_qst(self) class QSTEditWidget(gtk.VBox): def __init__(self, *a, **k): gtk.VBox.__init__(self, *a, **k) self._id = None def to_qst(self): pass def from_qst(self, content): pass def __str__(self): return "Unknown" def reset(self): pass def to_human(self): pass class QSTTextEditWidget(QSTEditWidget): label_text = _("Enter a message:") def __init__(self): QSTEditWidget.__init__(self, False, 2) lab = gtk.Label(self.label_text) lab.show() self.pack_start(lab, 0, 0, 0) self.__tb = gtk.TextBuffer() ta = gtk.TextView(self.__tb) ta.show() self.pack_start(ta, 1, 1, 1) def __str__(self): return self.__tb.get_text(self.__tb.get_start_iter(), self.__tb.get_end_iter()) def reset(self): self.__tb.set_text("") def to_qst(self): return str(self) def from_qst(self, content): self.__tb.set_text(content) def to_human(self): return str(self) class QSTFileEditWidget(QSTEditWidget): label_text = _("Choose a text file. The contents will be used when the QST is sent.") def __init__(self): QSTEditWidget.__init__(self, False, 2) lab = gtk.Label(self.label_text) lab.set_line_wrap(True) lab.show() self.pack_start(lab, 1, 1, 1) self.__fn = miscwidgets.FilenameBox() self.__fn.show() self.pack_start(self.__fn, 0, 0, 0) def __str__(self): return "Read: %s" % self.__fn.get_filename() def reset(self): self.__fn.set_filename("") def to_qst(self): return self.__fn.get_filename() def from_qst(self, content): self.__fn.set_filename(content) def to_human(self): return self.__fn.get_filename() class QSTExecEditWidget(QSTFileEditWidget): label_text = _("Choose a script to execute. The output will be used when the QST is sent") def __str__(self): return "Run: %s" % self.__fn.get_filename() class QSTGPSEditWidget(QSTEditWidget): msg_limit = 20 type = "GPS" def prompt_for_DPRS(self, button): dprs = do_dprs_calculator(self.__msg.get_text()) if dprs is None: return else: self.__msg.set_text(dprs) def __init__(self, config): QSTEditWidget.__init__(self, False, 2) lab = gtk.Label(_("Enter your GPS message:")) lab.set_line_wrap(True) lab.show() self.pack_start(lab, 1, 1, 1) hbox = gtk.HBox(False, 2) hbox.show() self.pack_start(hbox, 0, 0, 0) self.__msg = gtk.Entry(self.msg_limit) self.__msg.show() hbox.pack_start(self.__msg, 1, 1, 1) dprs = gtk.Button("DPRS") if not isinstance(self, QSTGPSAEditWidget): dprs.show() self.__msg.set_text(config.get("settings", "default_gps_comment")) else: self.__msg.set_text("ON D-RATS") dprs.connect("clicked", self.prompt_for_DPRS) hbox.pack_start(dprs, 0, 0, 0) def __str__(self): return "Message: %s" % self.__msg.get_text() def reset(self): self.__msg.set_text("") def to_qst(self): return self.__msg.get_text() def from_qst(self, content): self.__msg.set_text(content) def to_human(self): return self.__msg.get_text() class QSTGPSAEditWidget(QSTGPSEditWidget): msg_limit = 20 type = "GPS-A" class QSTRSSEditWidget(QSTEditWidget): label_string = _("Enter the URL of an RSS feed:") def __init__(self): QSTEditWidget.__init__(self, False, 2) lab = gtk.Label(self.label_string) lab.show() self.pack_start(lab, 1, 1, 1) self.__url = gtk.Entry() self.__url.set_text("http://") self.__url.show() self.pack_start(self.__url, 0, 0, 0) def __str__(self): return "Source: %s" % self.__url.get_text() def to_qst(self): return self.__url.get_text() def from_qst(self, content): self.__url.set_text(content) def reset(self): self.__url.set_text("") def to_human(self): return self.__url.get_text() class QSTCAPEditWidget(QSTRSSEditWidget): label_string = _("Enter the URL of a CAP feed:") class QSTStationEditWidget(QSTEditWidget): def ev_group_sel(self, group, station): group = group.get_active_text() if not self.__sources: return for src in self.__sources: if src.get_name() == group: break if src.get_name() != group: return marks = [x.get_name() for x in src.get_points()] store = station.get_model() store.clear() for i in sorted(marks): station.append_text(i) if len(marks): station.set_active(0) def __init__(self): QSTEditWidget.__init__(self, False, 2) lab = gtk.Label(_("Choose a station whose position will be sent")) lab.show() self.pack_start(lab, 1, 1, 1) hbox = gtk.HBox(False, 10) # This is really ugly, but to fix it requires more work self.__sources = mainapp.get_mainapp().map.get_map_sources() sources = [x.get_name() for x in self.__sources] self.__group = miscwidgets.make_choice(sources, False, _("Stations")) self.__group.show() hbox.pack_start(self.__group, 0, 0, 0) self.__station = miscwidgets.make_choice([], False) self.__station.show() hbox.pack_start(self.__station, 0, 0, 0) self.__group.connect("changed", self.ev_group_sel, self.__station) self.ev_group_sel(self.__group, self.__station) hbox.show() self.pack_start(hbox, 0, 0, 0) def to_qst(self): if not self.__group.get_active_text(): return None elif not self.__station.get_active_text(): return None else: return "%s::%s" % (self.__group.get_active_text(), self.__station.get_active_text()) def to_human(self): return "%s::%s" % (self.__group.get_active_text(), self.__station.get_active_text()) class QSTWUEditWidget(QSTEditWidget): label_text = _("Enter a WeatherUnderground station ID:") def __init__(self): QSTEditWidget.__init__(self) lab = gtk.Label(self.label_text) lab.show() self.pack_start(lab, 1, 1, 1) hbox = gtk.HBox(False, 2) hbox.show() self.pack_start(hbox, 0, 0, 0) self.__station = gtk.Entry() self.__station.show() hbox.pack_start(self.__station, 0, 0, 0) types = [_("Airport"), _("Personal")] self.__type = miscwidgets.make_choice(types, False, types[0]) self.__type.show() hbox.pack_start(self.__type, 0, 0, 0) def to_qst(self): return "%s/%s" % (self.__type.get_active_text(), self.__station.get_text()) def from_qst(self, content): try: t, s = content.split("/", 2) except: print "Unable to split `%s'" % content t = _("Airport") s = _("UNKNOWN") combo_select(self.__type, t) self.__station.set_text(s) def to_human(self): return self.to_qst() class QSTEditDialog(gtk.Dialog): def _select_type(self, box): wtype = box.get_active_text() if self.__current: self.__current.hide() self.__current = self._types[wtype] self.__current.show() def _make_controls(self): hbox = gtk.HBox(False, 2) self._type = make_choice(self._types.keys(), False, default=_("Text")) self._type.set_size_request(100, -1) self._type.show() self._type.connect("changed", self._select_type) hbox.pack_start(self._type, 0, 0, 0) intervals = ["1", "5", "10", "20", "30", "60", ":30", ":15"] self._freq = make_choice(intervals, True, default="60") self._freq.set_size_request(75, -1) self._freq.show() hbox.pack_start(self._freq, 0, 0, 0) hbox.show() return hbox def __init__(self, config, ident, parent=None): self._types = { _("Text") : QSTTextEditWidget(), _("File") : QSTFileEditWidget(), _("Exec") : QSTExecEditWidget(), _("GPS") : QSTGPSEditWidget(config), _("GPS-A"): QSTGPSAEditWidget(config), _("RSS") : QSTRSSEditWidget(), _("CAP") : QSTCAPEditWidget(), _("Station") : QSTStationEditWidget(), _("Weather (WU)") : QSTWUEditWidget(), } gtk.Dialog.__init__(self, parent=parent, buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) self._ident = ident self._config = config self.__current = None self.set_size_request(400, 150) self.vbox.pack_start(self._make_controls(), 0, 0, 0) for i in self._types.values(): i.set_size_request(-1, 80) self.vbox.pack_start(i, 0, 0, 0) if self._config.has_section(self._ident): combo_select(self._type, self._config.get(self._ident, "type")) self._freq.child.set_text(self._config.get(self._ident, "freq")) self._select_type(self._type) self.__current.from_qst(self._config.get(self._ident, "content")) else: self._select_type(self._type) def save(self): if not self._config.has_section(self._ident): self._config.add_section(self._ident) self._config.set(self._ident, "enabled", "True") self._config.set(self._ident, "freq", self._freq.get_active_text()) self._config.set(self._ident, "content", self.__current.to_qst()) self._config.set(self._ident, "type", self._type.get_active_text()) def get_qst_class(typestr): classes = { _("Text") : QSTText, _("Exec") : QSTExec, _("File") : QSTFile, _("GPS") : QSTGPS, _("GPS-A") : QSTGPSA, _("Station") : QSTStation, _("RSS") : QSTRSS, _("CAP") : QSTCAP, _("Weather (WU)") : QSTWeatherWU, } if not HAVE_FEEDPARSER: del classes[_("RSS")] return classes.get(typestr, None) d-rats-0.3.3/d_rats/reqobject.py000066400000000000000000000037061160617671700165350ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject import miscwidgets class RequestRemoteObjectUI(gtk.Dialog): def __init__(self, rpcsession, station, parent=None): gtk.Dialog.__init__(self, title="Request remote object", buttons=("Retrieve", gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), parent=parent) self.__list = miscwidgets.KeyedListWidget(\ [(gobject.TYPE_STRING, "_ID"), (gobject.TYPE_STRING, "Name"), (gobject.TYPE_STRING, "Info")]) self.__list.set_resizable(0, True) self.__list.show() sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add_with_viewport(self.__list) sw.show() self.vbox.pack_start(sw, 1, 1, 1) self.set_default_size(400, 400) def set_objects(self, objlist): for name, info in objlist: self.__list.set_item(name, name, info) def get_selected_item(self): try: return self.__list.get_item(self.__list.get_selected())[1] except Exception, e: print "Unable to get selected item: %s" % e return None d-rats-0.3.3/d_rats/session_coordinator.py000066400000000000000000000365761160617671700206600ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import socket import threading import time import os import gobject import formgui import emailgw import signals import msgrouting from utils import run_safe, run_gtk_locked from d_rats.sessions import base, file, form, sock class SessionThread(object): OUTGOING = False def __init__(self, coord, session, data): self.enabled = True self.coord = coord self.session = session self.arg = data self.thread = threading.Thread(target=self.worker, args=(data,)) self.thread.setDaemon(True) self.thread.start() def stop(self): self.enabled = False self.thread.join() def worker(self, **args): print "**** EMPTY SESSION THREAD ****" class FileBaseThread(SessionThread): progress_key = "recv_size" @run_safe def status_cb(self, vals): #print "GUI Status:" #for k,v in vals.items(): # print " -> %s: %s" % (k, v) if vals["total_size"]: pct = (float(vals[self.progress_key]) / vals["total_size"]) * 100.0 else: pct = 0.0 if vals["retries"] > 0: retries = " (%i retries)" % vals["retries"] else: retries = "" if vals.has_key("start_time"): elapsed = time.time() - vals["start_time"] kbytes = vals[self.progress_key] speed = " %2.2f B/s" % (kbytes / elapsed) else: speed = "" if vals["sent_wire"]: amt = vals["sent_wire"] if amt > 1024: sent = " (%s %.1f KB)" % (_("Total"), amt >> 10) else: sent = " (%s %i B)" % (_("Total"), amt) else: sent = "" msg = "%s [%02.0f%%]%s%s%s" % (vals["msg"], pct, speed, sent, retries) self.pct_complete = pct self.coord.session_status(self.session, msg) def completed(self, objname=None): self.coord.session_status(self.session, _("Transfer Completed")) if objname: msg = " of %s" % objname else: msg = "" size = self.session.stats["total_size"] if size > 1024: size >>= 10 units = "KB" else: units = "B" if self.session.stats.has_key("start_time"): start = self.session.stats["start_time"] exmsg = " (%i%s @ %2.2f B/s)" % (\ size, units, self.session.stats["total_size"] / (time.time() - start)) else: exmsg = "" def failed(self, restart_info=None): s = _("Transfer Interrupted") + \ " (%.0f%% complete)" % self.pct_complete self.coord.session_failed(self.session, s, restart_info) def __init__(self, *args): SessionThread.__init__(self, *args) self.pct_complete = 0.0 self.session.status_cb = self.status_cb class FileRecvThread(FileBaseThread): progress_key = "recv_size" def worker(self, path): fn = self.session.recv_file(path) if fn: self.completed("file %s" % os.path.basename(fn)) self.coord.session_newfile(self.session, fn) else: self.failed() class FileSendThread(FileBaseThread): OUTGOING = True progress_key = "sent_size" def worker(self, path): if self.session.send_file(path): self.completed("file %s" % os.path.basename(path)) self.coord.session_file_sent(self.session, path) else: self.failed((self.session.get_station(), path)) class FormRecvThread(FileBaseThread): progress_key = "recv_size" def worker(self, path): md = os.path.join(self.coord.config.form_store_dir(), _("Inbox")) newfn = time.strftime(os.path.join(md, "form_%m%d%Y_%H%M%S.xml")) if not msgrouting.msg_lock(newfn): print "AIEE! Unable to lock incoming new message file!" fn = self.session.recv_file(newfn) name = "%s %s %s" % (self.session.name, _("from"), self.session.get_station()) if fn == newfn: form = formgui.FormFile(fn) form.add_path_element(self.coord.config.get("user", "callsign")) form.save_to(fn) self.completed("form") self.coord.session_newform(self.session, fn) else: self.failed() print "<--- Form transfer failed -->" class FormSendThread(FileBaseThread): OUTGOING = True progress_key = "sent_size" def worker(self, path): if self.session.send_file(path): self.completed() self.coord.session_form_sent(self.session, path) else: self.failed((self.session.get_station(), path)) class SocketThread(SessionThread): def status(self): vals = self.session.stats if vals["retries"] > 0: retries = " (%i %s)" % (vals["retries"], _("retries")) else: retries = "" msg = "%i %s %s %i %s %s%s" % (vals["sent_size"], _("bytes"), _("sent"), vals["recv_size"], _("bytes"), _("received"), retries) self.coord.session_status(self.session, msg) def socket_read(self, sock, length, to=5): data = "" t = time.time() while (time.time() - t) < to : d = "" try: d = sock.recv(length - len(d)) except socket.timeout: continue if not d and not data: raise Exception("Socket is closed") data += d return data def worker(self, data): (sock, timeout) = data print "*** Socket thread alive (%i timeout)" % timeout sock.settimeout(timeout) while self.enabled: t = time.time() try: sd = self.socket_read(sock, 512, timeout) except Exception, e: print str(e) break print "Waited %f sec for socket" % (time.time() - t) try: rd = self.session.read(512) except base.SessionClosedError, e: print "Session closed" self.enabled = False break self.status() if sd: print "Sending socket data (%i)" % len(sd) self.session.write(sd) if rd: print "Sending radio data (%i)" % len(rd) sock.sendall(rd) print "Closing session" self.session.close() try: sock.close() except: pass print "*** Socket thread exiting" class SessionCoordinator(gobject.GObject): __gsignals__ = { "session-status-update" : signals.SESSION_STATUS_UPDATE, "session-started" : signals.SESSION_STARTED, "session-ended" : signals.SESSION_ENDED, "file-received" : signals.FILE_RECEIVED, "form-received" : signals.FORM_RECEIVED, "file-sent" : signals.FILE_SENT, "form-sent" : signals.FORM_SENT, } _signals = __gsignals__ def _emit(self, signal, *args): gobject.idle_add(self.emit, signal, *args) def session_status(self, session, msg): self._emit("session-status-update", session._id, msg) def session_newform(self, session, path): self._emit("form-received", session._id, path) def session_newfile(self, session, path): self._emit("file-received", session._id, path) def session_form_sent(self, session, path): self._emit("form-sent", session._id, path) def session_file_sent(self, session, path): self._emit("file-sent", session._id, path) def session_failed(self, session, msg, restart_info=None): self._emit("session-ended", session._id, msg, restart_info) def cancel_session(self, id, force=False): if id < 2: # Don't let them cancel Control or Chat return try: session = self.sm.sessions[id] except Exception, e: print "Session `%i' not found: %s" % (id, e) return if self.sthreads.has_key(id): del self.sthreads[id] session.close(force) def create_socket_listener(self, sport, dport, dest): if dport not in self.socket_listeners.keys(): print "Starting a listener for port %i->%s:%i" % (sport, dest, dport) self.socket_listeners[dport] = \ sock.SocketListener(self.sm, dest, sport, dport) print "Started" else: raise Exception("Listener for %i already active" % dport) def new_file_xfer(self, session, direction): msg = _("File transfer of %s started with %s") % (session.name, session._st) self.emit("session-status-update", session._id, msg) if direction == "in": dd = self.config.get("prefs", "download_dir") self.sthreads[session._id] = FileRecvThread(self, session, dd) elif direction == "out": of = self.outgoing_files.pop() self.sthreads[session._id] = FileSendThread(self, session, of) def new_form_xfer(self, session, direction): msg = _("Message transfer of %s started with %s") % (session.name, session._st) self.emit("session-status-update", session._id, msg) if direction == "in": dd = self.config.form_store_dir() self.sthreads[session._id] = FormRecvThread(self, session, dd) elif direction == "out": of = self.outgoing_forms.pop() self.sthreads[session._id] = FormSendThread(self, session, of) def new_socket(self, session, direction): msg = _("Socket session %s started with %s") % (session.name, session._st) self.emit("session-status-update", session._id, msg) to = float(self.config.get("settings", "sockflush")) try: foo, port = session.name.split(":", 2) port = int(port) except Exception, e: print "Invalid socket session name %s: %s" % (session.name, e) session.close() return if direction == "in": try: ports = self.config.options("tcp_in") for _portspec in ports: portspec = self.config.get("tcp_in", _portspec) p, h = portspec.split(",") p = int(p) if p == port: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((h, port)) self.sthreads[session._id] = SocketThread(self, session, (sock, to)) return raise Exception("Port %i not configured" % port) except Exception, e: msg = _("Error starting socket session: %s") % e self.emit("session-status-update", session._id, msg) session.close() elif direction == "out": sock = self.socket_listeners[port].dsock self.sthreads[session._id] = SocketThread(self, session, (sock, to)) @run_gtk_locked def _new_session(self, type, session, direction): if session._id <= 3: return # Skip control, chat, sniff, rpc print "New session (%s) of type: %s" % (direction, session.__class__) self.emit("session-started", session._id, type) if isinstance(session, form.FormTransferSession): self.new_form_xfer(session, direction) elif isinstance(session, file.FileTransferSession): self.new_file_xfer(session, direction) elif isinstance(session, sock.SocketSession): self.new_socket(session, direction) else: print "*** Unknown session type: %s" % session.__class__.__name__ def new_session(self, type, session, direction): gobject.idle_add(self._new_session, type, session, direction) def end_session(self, id): thread = self.sthreads.get(id, None) if isinstance(thread, SessionThread): del self.sthreads[id] else: self._emit("session-ended", id, "Ended", None) def session_cb(self, data, reason, session): t = str(session.__class__.__name__).replace("Session", "") if "." in t: t = t.split(".")[2] if reason.startswith("new,"): self.new_session(t, session, reason.split(",", 2)[1]) elif reason == "end": self.end_session(session._id) def send_file(self, dest, filename, name=None): if name is None: name = os.path.basename(filename) self.outgoing_files.insert(0, filename) print "Outgoing files: %s" % self.outgoing_files xfer = file.FileTransferSession bs = self.config.getint("settings", "ddt_block_size") ol = self.config.getint("settings", "ddt_block_outlimit") t = threading.Thread(target=self.sm.start_session, kwargs={"name" : name, "dest" : dest, "cls" : xfer, "blocksize" : bs, "outlimit" : ol}) t.setDaemon(True) t.start() print "Started Session" def send_form(self, dest, filename, name="Form"): self.outgoing_forms.insert(0, filename) print "Outgoing forms: %s" % self.outgoing_forms xfer = form.FormTransferSession t = threading.Thread(target=self.sm.start_session, kwargs={"name" : name, "dest" : dest, "cls" : xfer}) t.setDaemon(True) t.start() print "Started form session" def __init__(self, config, sm): gobject.GObject.__init__(self) self.sm = sm self.config = config self.sthreads = {} self.outgoing_files = [] self.outgoing_forms = [] self.socket_listeners = {} def shutdown(self): for dport, listener in self.socket_listeners.items(): print "Stopping TCP:%i" % dport listener.stop() d-rats-0.3.3/d_rats/sessionmgr.py000066400000000000000000000212041160617671700167410ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time import threading import os import struct import socket from ddt2 import DDT2EncodedFrame import transport from sessions import base, control, stateful, stateless from sessions import file, form, sock, sniff class SessionManager(object): def set_comm(self, pipe, **kwargs): self.pipe = pipe if self.tport: self.tport.disable() self.tport = transport.Transporter(self.pipe, inhandler=self.incoming, **kwargs) def set_call(self, callsign): self.station = callsign def __init__(self, pipe, station, **kwargs): self.pipe = self.tport = None self.station = station self.sniff_session = None self.last_frame = 0 self.sessions = {} self.session_cb = {} self.set_comm(pipe, **kwargs) self._sid_counter = 0 self._sid_lock = threading.Lock() self.control = control.ControlSession() self._register_session(self.control, "CQCQCQ", "new,out") self._stations_heard = {} def get_heard_stations(self): return dict(self._stations_heard) def manual_heard_station(self, station): self._stations_heard[station] = time.time() def fire_session_cb(self, session, reason): for f,d in self.session_cb.items(): try: f(d, reason, session) except Exception, e: print "Exception in session CB: %s" % e def register_session_cb(self, function, data): self.session_cb[function] = data for i,s in self.sessions.items(): self.fire_session_cb(s, "new,existing") def shutdown(self, force=False): if force: self.tport.disable() if self.sessions.has_key(self.control._id): del self.sessions[self.control._id] for s in self.sessions.values(): print "Stopping session `%s'" % s.name s.close(force) if not force: self.tport.disable() def incoming(self, frame): self.last_frame = time.time() if frame.s_station not in ["!"]: self._stations_heard[frame.s_station] = time.time() if self.sniff_session is not None: self.sessions[self.sniff_session].handler(frame) if frame.d_station != "CQCQCQ" and \ frame.d_station != self.station and \ frame.session != 1: # Not CQ, not us, and not chat print "Received frame for station `%s'" % frame.d_station return elif frame.s_station == self.station: # Either there is another station using our callsign, or # this packet arrived back at us due to a loop print "Received looped frame" return if not frame.session in self.sessions.keys(): print "Incoming frame for unknown session `%i'" % frame.session return session = self.sessions[frame.session] if session.stateless == False and \ session._st != frame.s_station: print "Received frame from invalid station `%s' (expecting `%s'" % (frame.s_station, session._st) return if session.handler: session.handler(frame) else: session.inq.enqueue(frame) session.notify() print "Received block %i:%i for session `%s'" % (frame.seq, frame.type, session.name) def outgoing(self, session, block): self.last_frame = time.time() if not block.d_station: block.d_station = session._st block.s_station = self.station if session._rs: block.session = session._rs else: block.session = session._id self.tport.send_frame(block) def _get_new_session_id(self): self._sid_lock.acquire() if self._sid_counter >= 255: for id in range(0, 255): if id not in self.sessions.keys(): self._sid_counter = id else: id = self._sid_counter self._sid_counter += 1 self._sid_lock.release() return id def _register_session(self, session, dest, reason): id = self._get_new_session_id() if id is None: # FIXME print "No free slots? I can't believe it!" session._sm = self session._id = id session._st = dest self.sessions[id] = session self.fire_session_cb(session, reason) return id def _deregister_session(self, id): if self.sessions.has_key(id): self.fire_session_cb(self.sessions[id], "end") try: del self.sessions[id] except Exception, e: print "No session %s to deregister" % id def start_session(self, name, dest=None, cls=None, **kwargs): if not cls: if dest: s = stateful.StatefulSession(name) else: s = stateless.StatelessSession(name) dest = "CQCQCQ" else: s = cls(name, **kwargs) s.set_state(base.ST_SYNC) id = self._register_session(s, dest, "new,out") if dest != "CQCQCQ": if not self.control.new_session(s): self._deregister_session(id) return s def set_sniffer_session(self, id): self.sniff_session = id def stop_session(self, session): for id, s in self.sessions.items(): if session.name == s.name: self.tport.flush_blocks(id) if session.get_state() != base.ST_CLSD: self.control.end_session(session) self._deregister_session(id) session.close() return True return False def end_session(self, id): try: del self.sessions[id] except Exception, e: print "Unable to deregister session" def get_session(self, rid=None, rst=None, lid=None): if not (rid or rst or lid): print "get_station() with no selectors!" return None for s in self.sessions.values(): if rid and s._rs != rid: continue if rst and s._st != rst: continue if lid and s._id != lid: continue return s return None if __name__ == "__main__": #p = transport.TestPipe(dst="KI4IFW") import comm import sys import sessions #if sys.argv[1] == "KI4IFW": # p = comm.SerialDataPath(("/dev/ttyUSB0", 9600)) #else: # p = comm.SerialDataPath(("/dev/ttyUSB0", 38400)) p = comm.SocketDataPath(("localhost", 9000)) #p.make_fake_data("SOMEONE", "CQCQCQ") p.connect() sm = SessionManager(p, sys.argv[1]) s = sm.start_session("chat", dest="CQCQCQ", cls=sessions.ChatSession) def cb(data, args): print "---------[ CHAT DATA ]------------" s.register_cb(cb) s.write("This is %s online" % sys.argv[1]) if sys.argv[1] == "KI4IFW": S = sm.start_session("xfer", "KI4IFW", cls=sessions.FileTransferSession) S.send_file("inputdialog.py") else: def h(data, reason, session): print "Session CB: %s" % reason if reason == "new,in": print "Receiving file" t = threading.Thread(target=session.recv_file, args=("/tmp",)) t.setDaemon(True) t.start() print "Done" sm.register_session_cb(h, None) try: while True: time.sleep(30) except Exception, e: print "------- Closing" sm.shutdown() # blocks = s.recv_blocks() # for b in blocks: # print "Chat message: %s: %s" % (b.get_info()[2], b.get_data()) d-rats-0.3.3/d_rats/sessions/000077500000000000000000000000001160617671700160455ustar00rootroot00000000000000d-rats-0.3.3/d_rats/sessions/__init__.py000066400000000000000000000000001160617671700201440ustar00rootroot00000000000000d-rats-0.3.3/d_rats/sessions/base.py000066400000000000000000000050551160617671700173360ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading from d_rats import transport T_STATELESS = 0 T_GENERAL = 1 T_UNUSED2 = 2 # Old non-pipelined FileTransfer T_UNUSED3 = 3 # Old non-pipelined FormTransfer T_SOCKET = 4 T_FILEXFER = 5 T_FORMXFER = 6 T_RPC = 7 ST_OPEN = 0 ST_CLSD = 1 ST_CLSW = 2 ST_SYNC = 3 class SessionClosedError(Exception): pass class Session(object): _sm = None _id = None _st = None _rs = None type = None def __init__(self, name): self.name = name self.inq = transport.BlockQueue() self.handler = None self.state_event = threading.Event() self.state = ST_CLSD self.stats = { "sent_size" : 0, "recv_size" : 0, "sent_wire" : 0, "recv_wire" : 0, "retries" : 0, } def send_blocks(self, blocks): for b in blocks: self._sm.outgoing(self, b) def recv_blocks(self): return self.inq.dequeue_all() def close(self, force=False): print "Got close request" if force: self.state = ST_CLSD if self._sm: self._sm.stop_session(self) def notify(self): pass def read(self): pass def write(self, dest="CQCQCQ"): pass def set_state(self, state): if state not in [ST_OPEN, ST_CLSD, ST_SYNC]: return False self.state = state self.state_event.set() self.notify() def get_state(self): return self.state def wait_for_state_change(self, timeout=None): before = self.state self.state_event.clear() self.state_event.wait(timeout) return self.state != before def get_station(self): return self._st def get_name(self): return self.name d-rats-0.3.3/d_rats/sessions/chat.py000066400000000000000000000156101160617671700173410ustar00rootroot00000000000000import random import time import gobject from d_rats import signals, platform, gps, utils, station_status from d_rats.version import DRATS_VERSION from d_rats.sessions import base, stateless from d_rats.ddt2 import DDT2EncodedFrame, DDT2RawData class ChatSession(stateless.StatelessSession, gobject.GObject): __gsignals__ = { "incoming-chat-message" : signals.INCOMING_CHAT_MESSAGE, "outgoing-chat-message" : signals.OUTGOING_CHAT_MESSAGE, "ping-request" : signals.PING_REQUEST, "ping-response" : signals.PING_RESPONSE, "incoming-gps-fix" : signals.INCOMING_GPS_FIX, "station-status" : signals.STATION_STATUS, "get-current-status" : signals.GET_CURRENT_STATUS, } _signals = __gsignals__ __cb = None __cb_data = None type = base.T_STATELESS T_DEF = 0 T_PNG_REQ = 1 T_PNG_RSP = 2 T_PNG_ERQ = 3 T_PNG_ERS = 4 T_STATUS = 5 compress = False def __init__(self, *args, **kwargs): stateless.StatelessSession.__init__(self, *args, **kwargs) gobject.GObject.__init__(self) self.set_ping_function() self.handler = self.incoming_data self.__ping_handlers = {} def set_ping_function(self, func=None): if func is not None: self.pingfn = func else: self.pingfn = self.ping_data def ping_data(self): p = platform.get_platform() return _("Running") + " D-RATS %s (%s)" % (DRATS_VERSION, p.os_version_string()) def _emit(self, signal, *args): gobject.idle_add(self.emit, signal, *args) def _incoming_chat(self, frame): self._emit("incoming-chat-message", frame.s_station, frame.d_station, unicode(frame.data, "utf-8")) def _incoming_gps(self, fix): self._emit("incoming-gps-fix", fix) def incoming_data(self, frame): print "Got chat frame: %s" % frame if frame.type == self.T_DEF: fix = gps.parse_GPS(frame.data) if fix and fix.valid: self._incoming_gps(fix) else: self._incoming_chat(frame) elif frame.type == self.T_PNG_REQ: self._emit("ping-request", frame.s_station, frame.d_station, "Request") if frame.d_station == "CQCQCQ": delay = random.randint(0,50) / 10.0 print "Broadcast ping, waiting %.1f sec" % delay time.sleep(delay) elif frame.d_station != self._sm.station: return # Not for us frame.d_station = frame.s_station frame.type = self.T_PNG_RSP try: frame.data = self.pingfn() except Exception, e: print "Ping function failed: %s" % e return self._sm.outgoing(self, frame) try: s, m = self.emit("get-current-status") self.advertise_status(s, m) except Exception, e: print "Exception while getting status for ping reply:" utils.log_exception() self._emit("ping-response", frame.s_station, frame.d_station, unicode(frame.data, "utf-8")) elif frame.type == self.T_PNG_RSP: print "PING OUT" self._emit("ping-response", frame.s_station, frame.d_station, frame.data) elif frame.type == self.T_PNG_ERQ: self._emit("ping-request", frame.s_station, frame.d_station, "%s %i %s" % (_("Echo request of"), len(frame.data), _("bytes"))) if frame.d_station == "CQCQCQ": delay = random.randint(0, 100) / 10.0 print "Broadcast ping echo, waiting %.1f sec" % delay time.sleep(delay) elif frame.d_station != self._sm.station: return # Not for us frame.d_station = frame.s_station frame.type = self.T_PNG_ERS self._sm.outgoing(self, frame) self._emit("ping-response", frame.s_station, frame.d_station, "%s %i %s" % (_("Echo of"), len(frame.data), _("bytes"))) elif frame.type == self.T_PNG_ERS: self._emit("ping-response", frame.s_station, frame.d_station, "%s %i %s" % (_("Echo of"), len(frame.data), _("bytes"))) if self.__ping_handlers.has_key(frame.s_station): cb, data = self.__ping_handlers[frame.s_station] try: cb(*data) except Exception: print "Exception while running ping callback" utils.log_exception() elif frame.type == self.T_STATUS: try: s = int(frame.data[0]) except Exception: print "Unable to parse station status: %s" % {frame.s_station : frame.data} s = 0 self._emit("station-status", frame.s_station, s, frame.data[1:]) def write_raw(self, data): f = DDT2RawData() f.data = data f.type = self.T_DEF print "Sending raw: %s" % data self._sm.outgoing(self, f) def write(self, data, dest="CQCQCQ"): self._emit("outgoing-chat-message", self._sm.station, self._st, data) stateless.StatelessSession.write(self, data, dest) def ping_station(self, station): f = DDT2EncodedFrame() f.d_station = station f.type = self.T_PNG_REQ f.data = "Ping Request" f.set_compress(False) self._sm.outgoing(self, f) self._emit("ping-request", f.s_station, f.d_station, "Request") def ping_echo_station(self, station, data, cb=None, *cbdata): if cb: self.__ping_handlers[station] = (cb, cbdata) f = DDT2EncodedFrame() f.d_station = station f.type = self.T_PNG_ERQ f.data = data f.set_compress(False) self._sm.outgoing(self, f) self._emit("ping-request", f.s_station, f.d_station, "%s %i %s" % (_("Echo of"), len(data), _("bytes"))) def advertise_status(self, stat, msg): if stat > station_status.STATUS_MAX or stat < station_status.STATUS_MIN: raise Exception("Status integer %i out of range" % stat) f = DDT2EncodedFrame() f.d_station = "CQCQCQ" f.type = self.T_STATUS f.data = "%i%s" % (stat, msg) self._sm.outgoing(self, f) d-rats-0.3.3/d_rats/sessions/control.py000066400000000000000000000151271160617671700201050ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import struct from d_rats.utils import log_exception from d_rats.ddt2 import DDT2EncodedFrame from d_rats.sessions import base, stateful, stateless from d_rats.sessions import file, form, sock T_PNG = 0 T_END = 1 T_ACK = 2 T_NEW = 3 class ControlSession(base.Session): stateless = True def ack_req(self, dest, data): f = DDT2EncodedFrame() f.type = T_ACK f.seq = 0 f.d_station = dest f.data = data self._sm.outgoing(self, f) def ctl_ack(self, frame): try: l, r = struct.unpack("BB", frame.data) session = self._sm.sessions[l] session._rs = r print "Signaled waiting session thread (l=%i r=%i)" % (l, r) except Exception, e: print "Failed to lookup new session event: %s" % e if session.get_state() == base.ST_CLSW: session.set_state(base.ST_CLSD) elif session.get_state() == base.ST_OPEN: pass elif session.get_state() == base.ST_SYNC: session.set_state(base.ST_OPEN) else: print "ACK for session in invalid state: %i" % session.get_state() def ctl_end(self, frame): print "End of session %s" % frame.data try: id = int(frame.data) except Exception, e: print "Session end request had invalid ID: %s" % e return try: session = self._sm.sessions[id] session.set_state(base.ST_CLSD) self._sm.stop_session(session) except Exception, e: print "Session %s ended but not registered" % id return frame.d_station = frame.s_station if session._rs: frame.data = str(session._rs) else: frame.data = str(session._id) self._sm.outgoing(self, frame) def ctl_new(self, frame): try: (id,) = struct.unpack("B", frame.data[:1]) name = frame.data[1:] except Exception, e: print "Session request had invalid ID: %s" % e return print "New session %i from remote" % id exist = self._sm.get_session(rid=id, rst=frame.s_station) if exist: print "Re-acking existing session %s:%i:%i" % (frame.s_station, id, exist._id) self.ack_req(frame.s_station, struct.pack("BB", id, exist._id)) return print "ACK'ing session request for %i" % id try: c = self.stypes[frame.type] print "Got type: %s" % c s = c(name) s._rs = id s.set_state(base.ST_OPEN) except Exception, e: log_exception() print "Can't start session type `%s': %s" % (frame.type, e) return num = self._sm._register_session(s, frame.s_station, "new,in") data = struct.pack("BB", id, num) self.ack_req(frame.s_station, data) def ctl(self, frame): if frame.d_station != self._sm.station: print "Control ignoring frame for station %s" % frame.d_station return if frame.type == T_ACK: self.ctl_ack(frame) elif frame.type == T_END: self.ctl_end(frame) elif frame.type >= T_NEW: self.ctl_new(frame) else: print "Unknown control message type %i" % frame.type def new_session(self, session): f = DDT2EncodedFrame() f.type = T_NEW + session.type f.seq = 0 f.d_station = session._st f.data = struct.pack("B", int(session._id)) + session.name wait_time = 5 for i in range(0,10): self._sm.outgoing(self, f) f.sent_event.wait(10) f.sent_event.clear() print "Sent request, blocking..." session.wait_for_state_change(wait_time) state = session.get_state() if state == base.ST_CLSD: print "Session is closed" break elif state == base.ST_SYNC: print "Waiting for synchronization" wait_time = 15 else: print "Established session %i:%i" % (session._id, session._rs) session.set_state(base.ST_OPEN) return True session.set_state(base.ST_CLSD) print "Failed to establish session" return False def end_session(self, session): if session.stateless: return while session.get_state() == base.ST_SYNC: print "Waiting for session in SYNC" session.wait_for_state_change(2) f = DDT2EncodedFrame() f.type = T_END f.seq = 0 f.d_station = session._st if session._rs: f.data = str(session._rs) else: f.data = str(session._id) session.set_state(base.ST_CLSW) for i in range(0, 3): print "Sending End-of-Session" self._sm.outgoing(self, f) f.sent_event.wait(10) f.sent_event.clear() print "Sent, waiting for response" session.wait_for_state_change(15) if session.get_state() == base.ST_CLSD: print "Session closed" return True session.set_state(base.ST_CLSD) print "Session closed because no response" return False def __init__(self): base.Session.__init__(self, "control") self.handler = self.ctl self.stypes = { T_NEW + base.T_GENERAL : stateful.StatefulSession, T_NEW + base.T_FILEXFER : file.FileTransferSession, T_NEW + base.T_FORMXFER : form.FormTransferSession, T_NEW + base.T_SOCKET : sock.SocketSession, } d-rats-0.3.3/d_rats/sessions/file.py000066400000000000000000000153011160617671700173360ustar00rootroot00000000000000import UserDict import struct import os import time import zlib from d_rats.sessions import base, stateful class NotifyDict(UserDict.UserDict): def __init__(self, cb, data={}): UserDict.UserDict.__init__(self) self.cb = cb self.data = data def __setitem__(self, name, value): self.data[name] = value self.cb() class FileTransferSession(stateful.StatefulSession): type = base.T_FILEXFER def internal_status(self, vals): print "XFER STATUS: %s" % vals["msg"] def status(self, msg): vals = dict(self.stats) vals["msg"] = msg vals["filename"] = self.filename self.status_cb(vals) self.last_status = msg def status_tick(self): self.status(self.last_status) def __init__(self, name, status_cb=None, **kwargs): stateful.StatefulSession.__init__(self, name, **kwargs) if not status_cb: self.status_cb = self.internal_status else: self.status_cb = status_cb self.sent_size = self.recv_size = 0 self.retries = 0 self.filename = "" self.last_status = "" self.stats = NotifyDict(self.status_tick, self.stats) self.stats["total_size"] = 0 def get_file_data(self, filename): f = file(filename, "rb") data = f.read() f.close() return data def put_file_data(self, filename, data): f = file(filename, "wb") f.write(data) f.close() def send_file(self, filename): data = self.get_file_data(filename) if not data: return False try: offer = struct.pack("I", len(data)) + os.path.basename(filename) self.write(offer) except base.SessionClosedError, e: print "Session closed while sending file information" return False self.filename = os.path.basename(filename) offset = None for i in range(40): print "Waiting for start" try: resp = self.read() except base.SessionClosedError, e: print "Session closed while waiting for start ack" return False if not resp: self.status(_("Waiting for response")) elif resp == "OK": self.status(_("Negotiation Complete")) offset = 0 break elif resp.startswith("RESUME:"): resume, _offset = resp.split(":", 1) print "Got RESUME request at %s" % _offset try: offset = int(_offset) except Exception, e: print "Unable to parse RESUME value: %s" % e offset = 0 self.status(_("Resuming at") + "%i" % offset) break else: print "Got unknown start: `%s'" % resp time.sleep(0.5) if offset is None: print "Did not get start response" return False self.stats["total_size"] = len(data) + len(offer) - offset self.stats["start_time"] = time.time() try: self.status("Sending") self.write(data[offset:], timeout=120) except base.SessionClosedError: print "Session closed while doing write" pass sent = self.stats["sent_size"] self.close() if sent != self.stats["total_size"]: self.status(_("Failed to send file (incomplete)")) return False else: actual = os.stat(filename).st_size self.stats["sent_size"] = self.stats["total_size"] = actual self.status(_("Complete")) return True def recv_file(self, dir): self.status(_("Waiting for transfer to start")) for i in range(40): try: data = self.read() except base.SessionClosedError, e: print "Session closed while waiting for start" return None if data: break else: time.sleep(0.5) if not data: self.status(_("No start block received!")) return None size, = struct.unpack("I", data[:4]) name = data[4:] if os.path.isdir(dir): filename = os.path.join(dir, name) else: filename = dir partfilename = filename + ".part" if os.path.exists(partfilename): data = self.get_file_data(self, partfilename) offset = os.path.getsize(partfilename) print "Part file exists, resuming at %i" % offset else: data = "" offset = 0 self.status(_("Receiving file") + \ " %s " % name + \ _("of size") + \ " %i" % size) self.stats["recv_size"] = offset self.stats["total_size"] = size self.stats["start_time"] = time.time() try: if offset: print "Sending resume at %i" % offset self.write("RESUME:%i" % offset) else: self.write("OK") except base.SessionClosedError, e: print "Session closed while sending start ack" return None self.status(_("Waiting for first block")) while True: try: d = self.read() except base.SessionClosedError: print "SESSION IS CLOSED" break if d: data += d self.status(_("Receiving")) try: self.put_file_data(filename, data) if os.path.exists(partfilename): print "Removing old file part" os.remove(partfilename) except Exception, e: print "Failed to write transfer data: %s" % e self.put_file_data(self, partfilename, data) return None if self.stats["recv_size"] != self.stats["total_size"]: self.status(_("Failed to receive file (incomplete)")) return None else: actual = os.stat(filename).st_size self.stats["recv_size"] = self.stats["total_size"] = actual self.status(_("Complete")) return filename def get_file_data(self, filename): f = file(filename, "rb") data = f.read() f.close() return zlib.compress(data, 9) def put_file_data(self, filename, zdata): try: data = zlib.decompress(zdata) f = file(filename, "wb") f.write(data) f.close() except zlib.error, e: raise e d-rats-0.3.3/d_rats/sessions/form.py000066400000000000000000000001701160617671700173600ustar00rootroot00000000000000from d_rats.sessions import base, file class FormTransferSession(file.FileTransferSession): type = base.T_FORMXFER d-rats-0.3.3/d_rats/sessions/rpc.py000066400000000000000000000417611160617671700172140ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time import datetime import os import glob import sys import gobject from d_rats import ddt2, signals, emailgw, wl2k # This feels wrong from d_rats.ui import main_events from d_rats.sessions import base, stateless from d_rats.version import DRATS_VERSION from d_rats.utils import log_exception ASCII_FS = "\x1C" ASCII_GS = "\x1D" ASCII_RS = "\x1E" ASCII_US = "\x1F" class UnknownRPCCall(Exception): pass def encode_dict(source): elements = [] for k, v in source.items(): if not isinstance(k, str): raise Exception("Cannot encode non-string dict key") if not isinstance(v, str): raise Exception("Cannoy encode non-string dict value") elements.append(k + ASCII_US + v) return ASCII_RS.join(elements) def decode_dict(string): result = {} if not string: return result elements = string.split(ASCII_RS) for element in elements: try: k, v = element.split(ASCII_US) except ValueError: raise Exception("Malformed dict encoding") result[k] = v return result class RPCJob(gobject.GObject): __gsignals__ = { "state-change" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT)), } STATES = ["complete", "timeout", "running"] def __init__(self, dest, desc): gobject.GObject.__init__(self) self.__dest = dest self.__desc = desc self._args = {} def get_dest(self): return self.__dest def get_desc(self): return self.__desc def set_state(self, state, result={}): if not isinstance(result, dict): raise Exception("Value of result property must be dict") if state in self.STATES: gobject.idle_add(self.emit, "state-change", state, result) else: raise Exception("Invalid status `%s'" % state) def unpack(self, raw): self._args = {} if not raw: self._args = {} else: self._args = decode_dict(raw) def pack(self): return encode_dict(self._args) def do(self, rpcactions): return {"rc" : "Unsupported Job Type"} class RPCFileListJob(RPCJob): def set_file_list(self, list): self._args = {} for item in list: self._args[item] = "" def get_file_list(self): return self._args.keys() def do(self, rpcactions): return rpcactions.RPC_file_list(self) class RPCFormListJob(RPCJob): def get_form_list(self): return [] def do(self, rpcactions): return rpcactions.RPC_form_list(self) class RPCPullFileJob(RPCJob): def set_file(self, filename): self._args = {"fn" : filename} def get_file(self): return self._args.get("fn", None) def do(self, rpcactions): return rpcactions.RPC_file_pull(self) class RPCDeleteFileJob(RPCJob): def set_file(self, filename): self._args["fn"] = filename def set_pass(self, passwd): self._args["passwd"] = passwd def get_file(self): return self._args.get("fn", None) def get_pass(self): return self._args.get("passwd", "") def do(self, rpcactions): return rpcactions.RPC_file_delete(self) class RPCPullFormJob(RPCJob): def set_form(self, form): self._args = {"fn" : form} def get_form(self): return self._args.get("fn", None) def do(self, rpcactions): return rpcactions.RPC_form_pull(self) class RPCPositionReport(RPCJob): def set_station(self, station): self._args = {"st" : station} def get_station(self): return self._args.get("st", "ERROR") def do(self, rpcactions): return rpcactions.RPC_pos_report(self) class RPCGetVersion(RPCJob): def do(self, rpcactions): return rpcactions.RPC_get_version(self) class RPCCheckMail(RPCJob): def do(self, rpcactions): return rpcactions.RPC_check_mail(self) def set_account(self, host, user, pasw, port, ssl): self._args = {"host" : host, "user" : user, "pasw" : pasw, "port" : port, "ssl" : ssl, } def get_account(self): return self._args["host"], self._args["user"], self._args["pasw"], \ int(self._args["port"]), self._args["ssl"] == "True" class RPCSession(gobject.GObject, stateless.StatelessSession): type = base.T_RPC T_RPCREQ = 0 T_RPCACK = 1 def __init__(self, *args, **kwargs): gobject.GObject.__init__(self) try: self.__rpcactions = kwargs["rpcactions"] del kwargs["rpcactions"] except KeyError: raise Exception("RPCSession requires RPCActionSet") stateless.StatelessSession.__init__(self, *args, **kwargs) self.__jobs = {} self.__jobq = [] self.__jobc = 0 self.__t_retry = 30 self.__enabled = True gobject.timeout_add(1000, self.__worker) self.handler = self.incoming_data def notify(self): pass def __decode_rpccall(self, frame): jobtype, args = frame.data.split(ASCII_GS) # FIXME: Make this more secure if not (jobtype.isalpha() and jobtype.startswith("RPC")): raise UnknownRPCCall("Unknown call `%s'" % jobtype) job = eval("%s('%s', 'New job')" % (jobtype, frame.s_station)) job.unpack(args) return job def __encode_rpccall(self, job): return "%s%s%s" % (job.__class__.__name__, ASCII_GS, job.pack()) def __get_seq(self): self.__jobc += 1 return self.__jobc def __job_to_frame(self, job, id): frame = ddt2.DDT2EncodedFrame() frame.type = self.T_RPCREQ frame.seq = id frame.data = self.__encode_rpccall(job) frame.d_station = job.get_dest() return frame def __send_job_status(self, id, station, state, result): frame = ddt2.DDT2EncodedFrame() frame.type = self.T_RPCACK frame.seq = id frame.data = result frame.d_station = station return frame def __job_state(self, job, state, _result, id): print "Job state: %s for %i: %s" % (state, id, _result) if state == "running": return result = encode_dict(_result) f = self.__send_job_status(id, job.get_dest(), state, result) self._sm.outgoing(self, f) def incoming_data(self, frame): if frame.type == self.T_RPCREQ: try: job = self.__decode_rpccall(frame) except UnknownRPCCall, e: print "Unable to execute RPC from %s: %s" % (frame.s_station, e) return job.connect("state-change", self.__job_state, frame.seq) result = job.do(self.__rpcactions) if result is not None: job.set_state("complete", result) elif frame.type == self.T_RPCACK: if self.__jobs.has_key(frame.seq): ts, att, job = self.__jobs[frame.seq] del self.__jobs[frame.seq] job.set_state("complete", decode_dict(frame.data)) else: print "Unknown job %i" % frame.seq else: print "Unknown RPC frame type %i" % frame.type def __send_job(self, job, id): print "Sending job `%s' to %s" % (job.get_desc(), job.get_dest()) frame = self.__job_to_frame(job, id) job.frame = frame self._sm.outgoing(self, frame) print "sent" def __worker(self): for id, (ts, att, job) in self.__jobs.items(): if job.frame and not job.frame.sent_event.isSet(): # Reset timer until the block is sent self.__jobs[id] = (time.time(), att, job) elif (time.time() - ts) > self.__t_retry: print "Cancelling job %i due to timeout" % id del self.__jobs[id] job.set_state("timeout") return True def submit(self, job): id = self.__get_seq() self.__send_job(job, id) self.__jobs[id] = (time.time(), 0, job) def stop(self): self.__enabled = False class RPCActionSet(gobject.GObject): __gsignals__ = { "rpc-send-file" : signals.RPC_SEND_FILE, "rpc-send-form" : signals.RPC_SEND_FORM, "get-message-list" : signals.GET_MESSAGE_LIST, "get-current-position" : signals.GET_CURRENT_POSITION, "user-send-chat" : signals.USER_SEND_CHAT, "event" : signals.EVENT, "register-object" : signals.REGISTER_OBJECT, "form-received" : signals.FORM_RECEIVED, "form-sent" : signals.FORM_SENT, } _signals = __gsignals__ def __init__(self, config, port): self.__config = config self.__port = port gobject.GObject.__init__(self) def __proxy_emit(self, signal): def handler(obj, *args): print "Proxy emit %s: %s" % (signal, args) gobject.idle_add(self.emit, signal, *args) return handler def RPC_pos_report(self, job): result = {} mycall = self.__config.get("user", "callsign") rqcall = job.get_station() if rqcall == mycall or rqcall == ".": rqcall = None try: fix = self.emit("get-current-position", rqcall) result["rc"] = "OK" except Exception, e: print "Exception while getting position of %s: " % rqcall log_exception() fix = None result["rc"] = "No data for station '%s'" % job.get_station() if fix: self.emit("user-send-chat", "CQCQCQ", self.__port, fix.to_NMEA_GGA(), True) print "[RPC] Position request for `%s'" % job.get_station() return result def RPC_file_list(self, job): result = {} dir = self.__config.get("prefs", "download_dir") files = glob.glob(os.path.join(dir, "*.*")) for fn in files: if os.path.isdir(fn): continue size = os.path.getsize(fn) if size < 1024: units = "B" else: size >>= 10 units = "KB" ds = datetime.datetime.fromtimestamp(os.path.getmtime(fn)) fn = os.path.basename(fn) result[fn] = "%i %s (%s)" % (size, units, ds.strftime("%Y-%m-%d %H:%M:%S")) event = main_events.Event(None, job.get_dest() + " " + \ _("Requested file list")) self.emit("event", event) return result def RPC_form_list(self, job): result = {} forms = self.emit("get-message-list", "CQCQCQ") for subj, stamp, filen in forms: ts = time.localtime(stamp) result[filen] = "%s/%s" % (subj, time.strftime("%b-%d-%Y %H:%M:%S", ts)) event = main_events.Event(None, job.get_dest() + " " + \ _("Requested message list")) self.emit("event", event) return result def RPC_file_pull(self, job): result = {} if not self.__config.getboolean("prefs", "allow_remote_files"): result["rc"] = "Remote file transfers not enabled" return result dir = self.__config.get("prefs", "download_dir") path = os.path.join(dir, job.get_file()) print "Remote requested %s" % path if os.path.exists(path): result["rc"] = "OK" self.emit("rpc-send-file", job.get_dest(), self.__port, path, job.get_file()) else: result["rc"] = "File not found" event = main_events.Event(None, job.get_dest() + " " + \ _("Requested file %s") % job.get_file()) self.emit("event", event) return result def RPC_file_delete(self, job): result = {} _permlist = self.__config.get("settings", "delete_from") try: permlist = _permlist.upper().split(",") except Exception: result["rc"] = "Access list not properly configured" return result if job.get_dest().upper() not in permlist: result["rc"] = "Access denied for %s" % job.get_dest() return result passwd = self.__config.get("settings", "remote_admin_passwd") if passwd and job.get_pass() != passwd: result["rc"] = "Access denied (Incorrect Password)" return result if "/" in job.get_file(): result["rc"] = "Access denied (file contains slash)" return result path = os.path.join(self.__config.get("prefs", "download_dir"), job.get_file()) if not os.path.exists(path): result["rc"] = "File not found (%s)" % job.get_file() return result try: os.remove(path) result["rc"] = "File %s deleted" % job.get_file() except Exception, e: result["rc"] = "Unable to delete %s: %s" % (job.get_file(), e) return result def RPC_form_pull(self, job): result = {} forms = self.emit("get-message-list", "CQCQCQ") result["rc"] = "Form not found" for subj, stamp, filen in forms: if filen == job.get_form(): fname = os.path.join(self.__config.platform.config_dir(), "messages", filen) if os.path.exists(fname): result["rc"] = "OK" self.emit("rpc-send-form", job.get_dest(), self.__port, fname, subj) break event = main_events.Event(None, job.get_dest() + " " + \ _("Requested message %s") % subj) self.emit("event", event) return result def RPC_get_version(self, job): result = {} result["version"] = DRATS_VERSION result["os"] = self.__config.platform.os_version_string() result["pyver"] = ".".join([str(x) for x in sys.version_info[:3]]) try: import gtk result["pygtkver"] = ".".join([str(x) for x in gtk.pygtk_version]) result["gtkver"] = ".".join([str(x) for x in gtk.gtk_version]) except ImportError: result["pygtkver"] = result["gtkver"] = "Unknown" return result def RPC_check_mail(self, job): def check_done(mt, success, message, job): result = { "rc" : success and "0" or "-1", "msg" : message } job.set_state("complete", result) event = main_events.Event(None, "%s %s: %s" % (_("Checking mail for"), job.get_dest(), message)) self.emit("event", event) args = job.get_account() + (job.get_dest(),) if args[0] == "@WL2K": if self.__config.getboolean("prefs", "msg_allow_wl2k"): mt = wl2k.WinLinkThread(self.__config, job.get_dest(), args[1]) mt.connect("event", self.__proxy_emit("event")) mt.connect("form-received", self.__proxy_emit("form-received")) mt.connect("form-sent", self.__proxy_emit("form-sent")) else: return {"rc" : "False", "msg" : "WL2K gateway is disabled"} else: if self.__config.getboolean("prefs", "msg_allow_pop3"): mt = emailgw.CoercedMailThread(self.__config, *args) else: return {"rc" : "False", "msg" : "POP3 gateway is disabled"} self.emit("register-object", mt) mt.connect("mail-thread-complete", check_done, job) mt.start() event = main_events.Event(None, "%s %s" % (job.get_dest(), _("requested a mail check"))) self.emit("event", event) d-rats-0.3.3/d_rats/sessions/sniff.py000066400000000000000000000040521160617671700175250ustar00rootroot00000000000000 import struct import gobject from d_rats.sessions import base, control, stateless session_types = { 4 : "General", 5 : "File", 6 : "Form", 7 : "Socket", 8 : "PFile", 9 : "PForm", } class SniffSession(stateless.StatelessSession, gobject.GObject): __gsignals__ = { "incoming_frame" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Src gobject.TYPE_STRING, # Dst gobject.TYPE_STRING, # Summary )) } def __init__(self, *a, **k): stateless.StatelessSession.__init__(self, *a, **k) gobject.GObject.__init__(self) self.handler = self._handler def decode_control(self, frame): if frame.type == control.T_ACK: l, r = struct.unpack("BB", frame.data) return _("Control: ACK") + " " + \ _("Local") + ":%i " % l + \ _("Remote") + ":%i" % r elif frame.type == control.T_END: return _("Control: END session %s") % frame.data elif frame.type >= control.T_NEW: id, = struct.unpack("B", frame.data[0]) name = frame.data[1:] stype = session_types.get(frame.type, "Unknown type %i" % frame.type) return _("Control: NEW session") +" %i: '%s' (%s)" % (id, name, stype) else: return _("Control: UNKNOWN") def _handler(self, frame): hdr = "%s->%s" % (frame.s_station, frame.d_station) if frame.s_station == "!": # Warm-up frame return if frame.session == 1: msg = "(%s: %s)" % (_("chat"), frame.data) elif frame.session == 0: msg = self.decode_control(frame) else: msg = "(S:%i L:%i)" % (frame.session, len(frame.data)) self.emit("incoming_frame", frame.s_station, frame.d_station, "%s %s" % (hdr, msg)) d-rats-0.3.3/d_rats/sessions/sock.py000066400000000000000000000041421160617671700173570ustar00rootroot00000000000000import socket from threading import Thread from d_rats.sessions import base, stateful class SocketSession(stateful.StatefulSession): type = base.T_SOCKET IDLE_TIMEOUT = None def __init__(self, name, status_cb=None): stateful.StatefulSession.__init__(self, name) if status_cb: self.status_cb = status_cb else: self.status_cb = self._status def _status(self, msg): print "Socket Status: %s" % msg class SocketListener(object): def __init__(self, sm, dest, sport, dport, addr='0.0.0.0'): self.sm = sm self.dest = dest self.sport = sport self.dport = dport self.addr = addr self.enabled = True self.lsock = None self.dsock = None self.thread = Thread(target=self.listener) self.thread.setDaemon(True) self.thread.start() def stop(self): self.enabled = False self.thread.join() def listener(self): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.settimeout(0.25) sock.bind(('0.0.0.0', self.sport)) sock.listen(0) self.lsock = sock name = "TCP:%i" % self.dport while self.enabled: try: (self.dsock, addr) = sock.accept() except socket.timeout: continue except Exception, e: print "Socket exception: %s" % e self.enabled = False break print "%i: Incoming socket connection from %s" % (self.dport, addr) s = self.sm.start_session(name=name, dest=self.dest, cls=SocketSession) while s.get_state() != base.ST_CLSD and self.enabled: s.wait_for_state_change(1) print "%s ended" % name self.dsock.close() self.dsock = None sock.close() print "TCP:%i shutdown" % self.dport d-rats-0.3.3/d_rats/sessions/stateful.py000066400000000000000000000407411160617671700202540ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading import time from d_rats import transport from d_rats.ddt2 import DDT2EncodedFrame from d_rats.sessions import base T_SYN = 0 T_ACK = 1 T_NAK = 2 T_DAT = 4 T_REQACK = 5 class StatefulSession(base.Session): stateless = False type = base.T_GENERAL IDLE_TIMEOUT = 90 def __init__(self, name, **kwargs): base.Session.__init__(self, name) self.outq = transport.BlockQueue() self.oob_queue = {} self.recv_list = [] self.outstanding = [] self.waiting_for_ack = [] self.enabled = True self.bsize = kwargs.get("blocksize", 1024) self.out_limit = kwargs.get("outlimit", 8) self.iseq = -1 self.oseq = 0 self.data = transport.BlockQueue() self.data_waiting = threading.Condition() self.__attempts = 0 self.__ack_timeout = 0 self.__full_acks = 0 self._rtr = 0.0 # Round trip rate (bps) self._xmt = 0.0 # Transmit rate (bps) self._xms = 0.0 # Start of last transmit of self.outstanding[] self._rtt_measure = { "bnum" : -1, "start" : 0, "end" : 0, "size" : 0, } self.event = threading.Event() self.thread = threading.Thread(target=self.worker) self.thread.setDaemon(True) self.thread.start() def notify(self): self.event.set() def close(self, force=False): print "Got close request, joining thread..." self.enabled = False self.notify() # Free up any block listeners if isinstance(self.outstanding, list): for b in self.outstanding: b.sent_event.set() b.sent_event.clear() b.ackd_event.set() elif self.outstanding: b.sent_event.set() self.thread.join() print "Thread is done, continuing with close" base.Session.close(self, force) def queue_next(self): if self.outstanding is None: # This is a silly race condition because the worker thread is # started in the init, which might run before we set our values # after the superclass init return limit = self.out_limit if self.__full_acks > 0: limit += self.__full_acks elif self.__full_acks < 0: limit -= abs(self.__full_acks) # Hard limit of 4KB outstanding (should be per-path!) hardlimit = (1 << 12) / self.bsize if limit < 2: limit = 2 elif limit > hardlimit: limit = hardlimit count = limit - len(self.outstanding) print "New limit is %i (%i/%i), queueing %i" % (limit, self.out_limit, hardlimit, count) if count < 0: # Need to requeue some blocks to shrink our window print "Need to requeue %i blocks to shrink window" % abs(count) for i in range(abs(count)): print " Requeuing block..." b = self.outstanding[-1] del self.outstanding[-1] self.outq.requeue(b) return elif count > 0: for i in range(count): b = self.outq.dequeue() if b: if b.seq == 0 and self.outstanding: print "### Pausing at rollover boundary ###" self.outq.requeue(b) break print "Queuing %i for send (%i)" % (b.seq, count) self.outstanding.append(b) else: break def is_timeout(self): if self._xms == 0: return True pending_size = 0 for block in self.outstanding: pending_size += block._xmit_z if pending_size == 0: return True if self._rtr != 0: rate = self._rtr else: # No measured rate yet so assume the minimum rate rate = 80 timeout = (pending_size / rate) * 1.5 if timeout < 12: # Don't allow small outgoing buffers to fool us into thinking # there is no turnaround delay timeout = 12 print "## Timeout for %i bytes @ %i bps: %.1f sec" % (pending_size, rate, timeout) print "## Remaining: %.1f sec" % (timeout - (time.time() - self._xms)) if self.__attempts: print "## Waiting for ACK, timeout in %i" % (self.__ack_timeout - time.time()) return (self.__ack_timeout - time.time()) <= 0 else: return (timeout - (time.time() - self._xms)) <= 0 def send_reqack(self, blocks): f = DDT2EncodedFrame() f.seq = 0 f.type = T_REQACK # FIXME: This needs to support 16-bit block numbers! f.data = "".join([chr(x) for x in blocks]) print "Requesting ack of blocks %s" % blocks self._sm.outgoing(self, f) def send_blocks(self): if self.outstanding and not self.is_timeout(): # Not time to try again yet return self.queue_next() if not self.outstanding: # nothing to send return if self.__attempts >= 10: print "Too many retries, closing..." self.set_state(base.ST_CLSD) self.enabled = False return # Short circuit to just an ack for outstanding blocks, if # we're still waiting for an ack from remote. Increase the timeout # for the ack by four seconds each time to give some backoff if self.waiting_for_ack: print "Didn't get last ack, asking again" self.send_reqack(self.waiting_for_ack) if self.__full_acks > 0: self.__full_acks = 0 else: self.__full_acks -= 1 self.__attempts += 1 self.__ack_timeout = time.time() + 4 + (self.__attempts * 4) return toack = [] self._rtt_measure["start"] = time.time() self._rtt_measure["end"] = self._rtt_measure["size"] = 0 self._xms = time.time() last_block = None for b in self.outstanding: if b.sent_event.isSet(): self.stats["retries"] += 1 b.sent_event.clear() print "Sending %i" % b.seq self._sm.outgoing(self, b) toack.append(b.seq) t = time.time() if last_block: last_block.sent_event.wait() self.update_xmt(last_block) self.stats["sent_wire"] += len(last_block.data) last_block = b self.send_reqack(toack) self.waiting_for_ack = toack print "Waiting for block to be sent" last_block.sent_event.wait() self._xme = time.time() self.update_xmt(last_block) self.stats["sent_wire"] += len(last_block.data) self.ts = time.time() print "Block sent after: %f" % (self.ts - t) def send_ack(self, blocks): f = DDT2EncodedFrame() f.seq = 0 f.type = T_ACK f.data = "".join([chr(x) for x in blocks]) print "Acking blocks %s (%s)" % (blocks, {"" : f.data}) self._sm.outgoing(self, f) def recv_blocks(self): blocks = self.inq.dequeue_all() blocks.reverse() def next(i): # FIXME: For 16 bit blocks return (i + 1) % 256 def enqueue(_block): self.data_waiting.acquire() self.data.enqueue(_block.data) self.iseq = _block.seq self.data_waiting.notify() self.data_waiting.release() for b in blocks: self._rtt_measure["size"] += len(b.get_packed()) if b.type == T_ACK: self.__attempts = 0 self._rtt_measure["end"] = time.time() self.waiting_for_ack = False acked = [ord(x) for x in b.data] print "Acked blocks: %s (/%i)" % (acked, len(self.outstanding)) for block in self.outstanding[:]: self._rtt_measure["size"] += block._xmit_z if block.seq in acked: block.ackd_event.set() self.stats["sent_size"] += len(block.data) self.outstanding.remove(block) else: print "Block %i outstanding, but not acked" % block.seq if len(self.outstanding) == 0: print "This ACKed every block" if self.__full_acks >= 0: self.__full_acks += 1 else: self.__full_acks = 0 else: print "This was not a full ACK" if self.__full_acks > 0: self.__full_acks = 0 else: self.__full_acks -= 1 elif b.type == T_DAT: print "Got block %i" % b.seq # FIXME: For 16-bit blocks if b.seq == 0 and self.iseq == 255: # Reset received list, because remote will only send # a block 0 following a block 255 if it has received # our ack of the previous 0-255 self.recv_list = [] if b.seq not in self.recv_list: self.recv_list.append(b.seq) self.stats["recv_size"] += len(b.data) self.oob_queue[b.seq] = b elif b.type == T_REQACK: toack = [] # FIXME: This needs to support 16-bit block numbers! for i in [ord(x) for x in b.data]: if i in self.recv_list: print "Acking block %i" % i toack.append(i) else: print "Naking block %i" % i self.send_ack(toack) else: print "Got unknown type: %i" % b.type if self.oob_queue: print "Waiting OOO blocks: %s" % self.oob_queue.keys() # Process any OOO blocks, if we should while next(self.iseq) in self.oob_queue.keys(): block = self.oob_queue[next(self.iseq)] print "Queuing now in-order block %i: %s" % (next(self.iseq), block) del self.oob_queue[next(self.iseq)] enqueue(block) def update_xmt(self, block): self._xmt = (self._xmt + block.get_xmit_bps()) / 2.0 print "Average transmit rate: %i bps" % self._xmt def calculate_rtt(self): rtt = self._rtt_measure["end"] - self._rtt_measure["start"] size = self._rtt_measure["size"] if size > 300: # Only calculate the rate if we had a reasonable amount of data # queued. We can't reliably measure small quantities, so we either # keep the last-known rate or leave it zero so that is_timeout() # will use a worst-case estimation self._rtr = size / rtt print "## Calculated rate for session %i: %.1f bps" % (self._id, self._rtr) print "## %i bytes in %.1f sec" % (size, self._rtt_measure["end"] - \ self._rtt_measure["start"]) self._rtt_measure["start"] = self._rtt_measure["end"] = 0 self._rtt_measure["size"] = 0 self._rtt_measure["bnum"] = -1 def worker(self): while self.enabled: self.send_blocks() self.recv_blocks() if self._rtt_measure["end"]: self.calculate_rtt() if not self.outstanding and self.outq.peek(): print "Short-circuit" continue # Short circuit because we have things to send print "Session loop (%s:%s)" % (self._id, self.name) if self.outstanding: print "Outstanding data, short sleep" self.event.wait(1) else: print "Deep sleep" self.event.wait(self.IDLE_TIMEOUT) if not self.event.isSet(): print "Session timed out!" self.set_state(base.ST_CLSD) self.enabled = False else: print "Awoke from deep sleep to some data" self.event.clear() def _block_read_for(self, count): waiting = self.data.peek_all() if not count and not waiting: self.data_waiting.wait(1) return if count > len("".join(waiting)): self.data_waiting.wait(1) return def _read(self, count): self.data_waiting.acquire() self._block_read_for(count) if count == None: b = self.data.dequeue_all() # BlockQueue.dequeue_all() returns the blocks in poppable order, # which is newest first b.reverse() buf = "".join(b) else: buf = "" i = 0 while True: next = self.data.peek() or '' if len(next) > 0 and (len(next) + i) < count: buf += self.data.dequeue() else: break self.data_waiting.release() return buf def read(self, count=None): while self.get_state() == base.ST_SYNC: print "Waiting for session to open" self.wait_for_state_change(5) if self.get_state() != base.ST_OPEN: raise base.SessionClosedError("State is %i" % self.get_state()) buf = self._read(count) if not buf and self.get_state() != base.ST_OPEN: raise base.SessionClosedError() return buf def write(self, buf, timeout=0): while self.get_state() == base.ST_SYNC: print "Waiting for session to open" self.wait_for_state_change(5) if self.get_state() != base.ST_OPEN: raise base.SessionClosedError("State is %s" % self.get_state()) blocks = [] while buf: chunk = buf[:self.bsize] buf = buf[self.bsize:] f = DDT2EncodedFrame() f.seq = self.oseq f.type = T_DAT f.data = chunk f.sent_event.clear() self.outq.enqueue(f) blocks.append(f) self.oseq = (self.oseq + 1) % 256 self.queue_next() self.event.set() while timeout is not None and \ blocks and \ self.get_state() != base.ST_CLSD: block = blocks[0] del blocks[0] print "Waiting for block %i to be ack'd" % block.seq block.sent_event.wait() if block.sent_event.isSet(): print "Block %i is sent, waiting for ack" % block.seq block.ackd_event.wait(timeout) if block.ackd_event.isSet() and block.sent_event.isSet(): print "%i ACKED" % block.seq else: print "%i Not ACKED (probably canceled)" % block.seq break else: print "Block %i not sent?" % block.seq d-rats-0.3.3/d_rats/sessions/stateless.py000066400000000000000000000023431160617671700204300ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from d_rats.ddt2 import DDT2EncodedFrame from d_rats.sessions import base class StatelessSession(base.Session): stateless = True type = base.T_STATELESS compress = True T_DEF = 0 def read(self): f = self.inq.dequeue() return f.s_station, f.d_station, f.data def write(self, data, dest="CQCQCQ"): f = DDT2EncodedFrame() f.seq = 0 f.type = self.T_DEF f.d_station = dest f.data = data f.set_compress(self.compress) self._sm.outgoing(self, f) d-rats-0.3.3/d_rats/signals.py000066400000000000000000000127251160617671700162200ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gobject STATUS = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) USER_STOP_SESSION = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Port Name USER_CANCEL_SESSION = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Port Name USER_SEND_FORM = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING, # Port Name gobject.TYPE_STRING, # Filename gobject.TYPE_STRING)) # Session name RPC_SEND_FORM = USER_SEND_FORM USER_SEND_FILE = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING, # Port Name gobject.TYPE_STRING, # Filename gobject.TYPE_STRING)) # Session name RPC_SEND_FILE = USER_SEND_FILE USER_SEND_CHAT = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING, # Port Name gobject.TYPE_STRING, # Text gobject.TYPE_BOOLEAN)) # Raw INCOMING_CHAT_MESSAGE = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Source gobject.TYPE_STRING, # Destination gobject.TYPE_STRING)) # Text OUTGOING_CHAT_MESSAGE = INCOMING_CHAT_MESSAGE GET_STATION_LIST = \ (gobject.SIGNAL_ACTION, gobject.TYPE_PYOBJECT, ()) GET_MESSAGE_LIST = \ (gobject.SIGNAL_ACTION, gobject.TYPE_PYOBJECT, (gobject.TYPE_STRING,)) # Station SUBMIT_RPC_JOB = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, # Job gobject.TYPE_STRING)) # Port Name EVENT = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) # Event NOTICE = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) CONFIG_CHANGED = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) SHOW_MAP_STATION = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) # Station PING_STATION = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING)) # Port Name PING_STATION_ECHO = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING, # Port Name gobject.TYPE_STRING, # Data gobject.TYPE_PYOBJECT, # Callback gobject.TYPE_PYOBJECT)) # Callback data PING_REQUEST = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Source gobject.TYPE_STRING, # Destination gobject.TYPE_STRING)) # Data PING_RESPONSE = PING_REQUEST INCOMING_GPS_FIX = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) # Fix STATION_STATUS = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station, gobject.TYPE_INT, # Status, gobject.TYPE_STRING)) # Status message GET_CURRENT_STATUS = \ (gobject.SIGNAL_ACTION, gobject.TYPE_PYOBJECT, ()) GET_CURRENT_POSITION = \ (gobject.SIGNAL_ACTION, gobject.TYPE_PYOBJECT, (gobject.TYPE_STRING,)) # Station (None for self) SESSION_STARTED = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Type SESSION_ENDED = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING, # Message, gobject.TYPE_PYOBJECT)) # Restart info SESSION_STATUS_UPDATE = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Message FILE_RECEIVED = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Filename FORM_RECEIVED = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Filename FILE_SENT = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Filename FORM_SENT = \ (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, # Session ID gobject.TYPE_STRING)) # Filename GET_CHAT_PORT = \ (gobject.SIGNAL_ACTION, gobject.TYPE_STRING, ()) TRIGGER_MSG_ROUTER = \ (gobject.SIGNAL_ACTION, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) # account (section) to trigger, "" if msgrouter REGISTER_OBJECT = \ (gobject.SIGNAL_ACTION, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) # Object to register d-rats-0.3.3/d_rats/spell.py000066400000000000000000000075121160617671700156750ustar00rootroot00000000000000import os import subprocess class Spelling: def __open_aspell(self): kwargs = {} if subprocess.mswindows: su = subprocess.STARTUPINFO() su.dwFlags |= subprocess.STARTF_USESHOWWINDOW su.wShowWindow = subprocess.SW_HIDE kwargs["startupinfo"] = su p = subprocess.Popen([self.__aspell, "pipe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, #close_fds=True, **kwargs) return p def __close_aspell(self): if self.__pipe: self.__pipe.terminate() self.__pipe = None def __init__(self, aspell="aspell", persist=True): self.__aspell = aspell self.__persist = persist self.__pipe = None def lookup_word(self, wiq): for c in wiq: c = ord(c) if c < ord('A') or c > ord('z') or \ (c > ord('Z') and c < ord('a')): return [] try: self.__pipe.stdout.readline() except Exception, e: print "Demand-opening aspell..." self.__pipe = self.__open_aspell() self.__pipe.stdout.readline() self.__pipe.stdin.write("%s%s" % (wiq, os.linesep)) suggest_str = self.__pipe.stdout.readline() if not self.__persist: self.__close_aspell() if suggest_str.startswith("*"): return [] elif not suggest_str.startswith("&"): raise Exception("Unknown response from aspell: %s" % suggest_str) suggestions = suggest_str.split() return suggestions[4:] def test(self): try: s = self.lookup_word("speling") if s[0] != "spelling,": print "Unable to validate first suggestion of `spelling'" print s[0] return False except Exception, e: print "Spelling test failed: %s" % e return False print "Tested spelling okay: %s" % s return True def test_word(spell, word): spell.stdin.write(word + "\n") result = spell.stdout.readline() spell.stdout.readline() if result.startswith("*"): return [] elif result.startswith("&"): items = result.split() return items[4:] else: print "Unknown response: `%s'" % result SPELL = None def get_spell(): global SPELL if not SPELL: SPELL = Spelling() return SPELL def __do_fly_spell(buffer): cursor_mark = buffer.get_mark("insert") start_iter = buffer.get_iter_at_mark(cursor_mark) end_iter = buffer.get_iter_at_mark(cursor_mark) if not start_iter.starts_word(): start_iter.backward_word_start() if end_iter.inside_word(): end_iter.forward_word_end() text = buffer.get_text(start_iter, end_iter) word = text.strip() #print "Got: '%s' (%s)" % (text, word) if not word: return end_iter.backward_chars(len(text) - len(word)) if " " in word: mispelled = False else: speller = get_spell() mispelled = bool(speller.lookup_word(word)) if text.endswith(" ") and mispelled: buffer.apply_tag_by_name("misspelled", start_iter, end_iter) else: buffer.remove_tag_by_name("misspelled", start_iter, end_iter) def prepare_TextBuffer(buf): import gtk import pango tags = buf.get_tag_table() tag = gtk.TextTag("misspelled") tag.set_property("underline", pango.UNDERLINE_SINGLE) tag.set_property("underline-set", True) tag.set_property("foreground", "red") tags.add(tag) buf.connect("changed", __do_fly_spell) if __name__ == "__main__": s = Spelling() print s.lookup_word("speling") print s.lookup_word("teh") print s.lookup_word("foo") d-rats-0.3.3/d_rats/station_status.py000066400000000000000000000031111160617671700176310ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . STATUS_MAX = 9 STATUS_MIN = 0 STATUS_UNKNOWN = 0 STATUS_ONLINE = 1 STATUS_UNATTENDED = 2 STATUS_OFFLINE = 9 __STATUS_MSGS = { STATUS_UNKNOWN : "Unknown", STATUS_ONLINE : "Online", STATUS_UNATTENDED : "Unattended", STATUS_OFFLINE : "Offline", } def get_status_msgs(): d = {} for k,v in __STATUS_MSGS.items(): d[k] = _(v) return d def get_status_vals(): d = {} for k,v in __STATUS_MSGS.items(): d[_(v)] = k return d class Station: def __init__(self, callsign): self.__call = callsign self.__heard = 0 self.__port = "" def set_heard(self, heard): self.__heard = heard def get_heard(self): return self.__heard def set_port(self, port): self.__port = port def get_port(self): return self.__port def __str__(self): return self.__call d-rats-0.3.3/d_rats/subst.py000066400000000000000000000023531160617671700157140ustar00rootroot00000000000000#!/usr/bin/python import ConfigParser import platform sublist = None class SubstitutionList(object): delim = "/" def __init__(self, configfile): self.config = ConfigParser.ConfigParser() self.config.read(configfile) def get_sub(self, key): if not self.config.has_section("subs") or \ not self.config.has_option("subs", key): return "" return self.config.get("subs", key) def subst(self, string): while string.count(self.delim) >= 2: first, _, rest = string.partition(self.delim) key, _, last = rest.partition(self.delim) sub = self.get_sub(key) print "Substitution for %s was: %s" % (key, sub) string = first + sub + last return string def load_subs(): global sublist if sublist: return True f = platform.get_platform().config_file("subst.conf") if not f: return False sublist = SubstitutionList(f) return True def subst_string(string): if not load_subs(): print "Unable to load substitution list" return string else: return sublist.subst(string) if __name__ == "__main__": print subst_string("Status: /10-14/") d-rats-0.3.3/d_rats/transport.py000066400000000000000000000303141160617671700166060ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading import re import time import random import traceback import sys import utils import ddt2 import comm class BlockQueue(object): def __init__(self): self._lock = threading.Lock() self._queue = [] def enqueue(self, block): self._lock.acquire() self._queue.insert(0, block) self._lock.release() def requeue(self, block): self._lock.acquire() self._queue.append(block) self._lock.release() def dequeue(self): self._lock.acquire() try: b = self._queue.pop() except IndexError: b = None self._lock.release() return b def dequeue_all(self): self._lock.acquire() l = self._queue self._queue = [] self._lock.release() return l def peek(self): self._lock.acquire() try: el = self._queue[0] except: el = None self._lock.release() return el def peek_all(self): self._lock.acquire() q = self._queue self._lock.release() return q # BE CAREFUL WITH THESE! def lock(self): self._lock.acquire() def unlock(self): self._lock.release() class Transporter(object): def __init__(self, pipe, inhandler=None, authfn=None, **kwargs): self.inq = BlockQueue() self.outq = BlockQueue() self.pipe = pipe self.inbuf = "" self.enabled = True self.inhandler = inhandler self.compat = kwargs.get("compat", False) self.warmup_length = kwargs.get("warmup_length", 8) self.warmup_timeout = kwargs.get("warmup_timeout", 3) self.force_delay = kwargs.get("force_delay", 0) self.compat_delay = kwargs.get("compat_delay", 5) self.msg_fn = kwargs.get("msg_fn", None) self.name = kwargs.get("port_name", "") self.thread = threading.Thread(target=self.worker, args=(authfn,)) self.thread.setDaemon(True) self.thread.start() self.last_xmit = 0 self.last_recv = 0 def __send(self, data): for i in range(0, 10): try: return self.pipe.write(data) except comm.DataPathIOError, e: if not self.pipe.can_reconnect: break print "Data path IO error: %s" % e try: time.sleep(i) print "Attempting reconnect..." self.pipe.reconnect() except comm.DataPathNotConnectedError: pass raise comm.DataPathIOError("Unable to reconnect") def __recv(self): data = "" for i in range(0, 10): try: return self.pipe.read_all_waiting() except comm.DataPathIOError, e: if not self.pipe.can_reconnect: break print "Data path IO error: %s" % e try: time.sleep(i) print "Attempting reconnect..." self.pipe.reconnect() except comm.DataPathNotConnectedError: pass raise comm.DataPathIOError("Unable to reconnect") def get_input(self): chunk = self.__recv() if chunk: self.inbuf += chunk self.last_recv = time.time() def _handle_frame(self, frame): if self.inhandler: self.inhandler(frame) else: self.inq.enqueue(frame) def parse_blocks(self): while ddt2.ENCODED_HEADER in self.inbuf and \ ddt2.ENCODED_TRAILER in self.inbuf: s = self.inbuf.index(ddt2.ENCODED_HEADER) e = self.inbuf.index(ddt2.ENCODED_TRAILER) + \ len(ddt2.ENCODED_TRAILER) if e < s: # Excise the extraneous end _tmp = self.inbuf[:e-len(ddt2.ENCODED_TRAILER)] + \ self.inbuf[e:] self.inbuf = _tmp continue block = self.inbuf[s:e] self.inbuf = self.inbuf[e:] f = ddt2.DDT2EncodedFrame() try: if f.unpack(block): print "Got a block: %s" % f self._handle_frame(f) elif self.compat: self._send_text_block(block) else: print "Found a broken block (S:%i E:%i len(buf):%i" % (\ s, e, len(self.inbuf)) utils.hexprint(block) except Exception, e: print "Failed to process block:" utils.log_exception() def _match_gps(self): # NMEA-style m = re.search("((?:\$GP[^\*]+\*[A-f0-9]{2}\r?\n?){1,2}.{8},.{20})", self.inbuf) if m: return m.group(1) # GPS-A style m = re.search("(\$\$CRC[A-z0-9]{4},[^\r]*\r)", self.inbuf) if m: return m.group(1) if "$$CRC" in self.inbuf: print "Didn't match:\n%s" % repr(self.inbuf) return None def _send_text_block(self, string): f = ddt2.DDT2RawData() f.seq = 0 f.session = 1 # Chat (for now) f.s_station = "CQCQCQ" f.d_station = "CQCQCQ" f.data = utils.filter_to_ascii(string) self._handle_frame(f) def _parse_gps(self): result = self._match_gps() if result: self.inbuf = self.inbuf.replace(result, "") print "Found GPS string: %s" % repr(result) self._send_text_block(result) else: return None def parse_gps(self): while self._match_gps(): self._parse_gps() def send_frames(self): delayed = False while True: f = self.outq.dequeue() if not f: break if self.force_delay and not delayed: if self.force_delay < 0: # If force_delay is negative, wait between 0.5 and # abs(force_delay) seconds before transmitting delay = random.randint(5, abs(self.force_delay)*10)/10.0 else: # If force_delay is positive, then wait exactly that # long before transmitting delay = self.force_delay print "Waiting %.1f sec before transmitting" % delay time.sleep(delay) delayed = True if ((time.time() - self.last_xmit) > self.warmup_timeout) and \ (self.warmup_timeout > 0): warmup_f = ddt2.DDT2EncodedFrame() warmup_f.seq = 0 warmup_f.session = 0 warmup_f.type = 254 warmup_f.s_station = "!" warmup_f.d_station = "!" warmup_f.data = ("\x01" * self.warmup_length) warmup_f.set_compress(False) print "Sending warm-up: %s" % warmup_f self.__send(warmup_f.get_packed()) print "Sending block: %s" % f f._xmit_s = time.time() self.__send(f.get_packed()) f._xmit_e = time.time() f.sent_event.set() self.last_xmit = time.time() def compat_is_time(self): return (time.time() - self.last_recv) > self.compat_delay def worker(self, authfn): if not self.pipe.is_connected(): if self.msg_fn: self.msg_fn("Connecting") try: self.pipe.connect() except comm.DataPathNotConnectedError, e: if self.msg_fn: self.msg_fn("Unable to connect (%s)" % e) print "Comm %s did not connect: %s" % (self.pipe, e) return if authfn and not authfn(self.pipe): if self.msg_fn: self.msg_fn("Authentication failed") self.enabled = False elif self.msg_fn: self.msg_fn("Connected") while self.enabled: try: self.get_input() except Exception, e: print "Exception while getting input: %s" % e utils.log_exception() self.enabled = False break self.parse_blocks() self.parse_gps() if self.inbuf and self.compat_is_time(): if self.compat: self._send_text_block(self.inbuf) else: print "### Unconverted data: %s" % self.inbuf self.inbuf = "" try: self.send_frames() except Exception, e: print "Exception while sending frames: %s" % e self.enabled = False break def disable(self): self.inhandler = None self.enabled = False self.thread.join() def send_frame(self, frame): if not self.enabled: print "Refusing to queue block for dead transport" return self.outq.enqueue(frame) def recv_frame(self): return self.inq.dequeue() def flush_blocks(self, id): # This should really call a flush method in the blockqueue with a # test function self.outq.lock() for b in self.outq._queue[:]: if b.session == id: print "Flushing block: %s" % b try: self.outq._queue.remove(b) except ValueError: print "Block disappeared while flushing?" self.outq.unlock() def __str__(self): return str(self.pipe) class TestPipe(object): def make_fake_data(self, src, dst): self.buf = "" for i in range(10): f = ddt2.DDT2EncodedFrame() f.s_station = src f.d_station = dst f.type = 1 f.seq = i f.session = 0 f.data = "This is a test frame to parse" self.buf += "asg;sajd;jsadnkbasdl;b as;jhd[SOB]laskjhd" + \ "asdkjh[EOB]a;klsd" + f.get_packed() + "asdljhasd[EOB]" + \ "asdljb alsjdljn[asdl;jhas" if i == 5: self.buf += "$GPGGA,075519,4531.254,N,12259.400,W,1,3,0,0.0,M,0,M,,*55\r\nK7HIO ,GPS Info\r" elif i == 7: self.buf += "$$CRC6CD1,Hills-Water-Treat-Plt>APRATS,DSTAR*:@233208h4529.05N/12305.91W>Washington County ARES;Hills Water Treat Pl\r\n" elif i == 2: self.buf += \ """$GPGGA,023531.36,4531.4940,N,12254.9766,W,1,07,1.3,63.7,M,-21.4,M,,*64\r\n$GPRMC,023531.36,A,4531.4940,N,12254.9766,W,0.00,113.7,010808,17.4,E,A*27\rK7TAY M ,/10-13/\r""" print "Made some data: %s" % self.buf def __init__(self, src="Sender", dst="Recvr"): self.make_fake_data(src, dst) def read(self, count): if not self.buf: return "" num = random.randint(1,count) b = self.buf[:num] self.buf = self.buf[num:] return b def write(self, buf): pass def test_simple(): p = TestPipe() t = Transporter(p) f = ddt2.DDT2EncodedFrame() f.seq = 9 f.type = 8 f.session = 7 f.d_station = "You" f.s_station = "Me" f.data = "ACK" t.send_frame(f) time.sleep(2) f = t.recv_frame() print "Received block: %s" % f t.disable() if __name__ == "__main__": test_simple() d-rats-0.3.3/d_rats/ui/000077500000000000000000000000001160617671700146145ustar00rootroot00000000000000d-rats-0.3.3/d_rats/ui/__init__.py000066400000000000000000000000001160617671700167130ustar00rootroot00000000000000d-rats-0.3.3/d_rats/ui/conntest.py000066400000000000000000000337601160617671700170340ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject try: from gtk import Assistant as baseclass except ImportError: print "No Assistant support" from d_rats.geocode_ui import baseclass if __name__ == "__main__": def _(string): return string TEST_TYPE_FIXEDMULTI = 0 TEST_TYPE_GRADMULTI = 1 def calc_watchdog(size): size += 35 # Packetization overhead bytes_per_sec = 950 / 8 # 950 bits per second sec = 10 + (size / bytes_per_sec) # Time to transmit, plus padding print "Waiting %i seconds for send of %i" % (sec, size) return int(sec * 1000) class ConnTestAssistant(baseclass): __gsignals__ = { "ping-echo-station" : (gobject.SIGNAL_ACTION, gobject.TYPE_NONE, (gobject.TYPE_STRING, # Station gobject.TYPE_STRING, # Port gobject.TYPE_STRING, # Data gobject.TYPE_PYOBJECT, # Callback gobject.TYPE_PYOBJECT)),# Callback data } def make_start_page(self, station, port): vbox = gtk.VBox(False, 0) def set_station(entry): self.__station = entry.get_text() self.set_page_complete(vbox, bool(self.__station)) def set_type(rb, type): self.__type = type for v in self.__tests.values(): v.hide() self.__tests[type].show() box = gtk.HBox(False, 0) slb = gtk.Label(_("Remote station:")) slb.show() sta = gtk.Entry(8) sta.set_text(station) sta.set_sensitive(False) sta.connect("changed", set_station) sta.show() plb = gtk.Label(_("Port:")) plb.show() prt = gtk.Entry() prt.set_text(port) prt.set_sensitive(False) prt.show() box.pack_start(slb, 0, 0, 0) box.pack_start(sta, 0, 0, 0) box.pack_start(plb, 0, 0, 0) box.pack_start(prt, 0, 0, 0) box.show() vbox.pack_start(box, 0, 0, 0) frame = gtk.Frame("Test Type") frame.show() box = gtk.VBox(False, 0) rb1 = gtk.RadioButton(None, _("Multiple fixed-size packets")) rb1.connect("clicked", set_type, TEST_TYPE_FIXEDMULTI) rb1.show() rb2 = gtk.RadioButton(rb1, _("Gradually increasing packet sizes")) rb2.connect("clicked", set_type, TEST_TYPE_GRADMULTI) rb2.show() box.pack_start(rb1, 0, 0, 0) box.pack_start(rb2, 0, 0, 0) box.show() frame.add(box) vbox.pack_start(frame, 0, 0, 0) vbox.show() return vbox def __make_grid(self, table, gridspec): vals = {} row = 0 def set_value(spin, scrolltype, name): self.__values[name] = spin.get_value() return False for l, s, i, u in gridspec: lab = gtk.Label(l + ":") lab.show() table.attach(lab, 0, 1, row, row+1, gtk.SHRINK) adj = gtk.Adjustment(s, i, u, i) val = gtk.SpinButton(adj, digits=0) val.connect("input", set_value, l) val.show() table.attach(val, 1, 2, row, row+1, gtk.SHRINK) set_value(val, None, l) row += 1 def make_gradmulti_settings(self): table = gtk.Table(8, 2) rows = [ (_("Attempts per size"), 3, 1, 10), (_("Increment size"), 256, 128, 1024), (_("Starting size"), 256, 256, 2048), (_("Ending size"), 1024, 256, 4096)] self.__make_grid(table, rows) return table def make_fixedmulti_settings(self): table = gtk.Table(2, 2) rows = [ (_("Packet size"), 256, 128, 4096), (_("Number of packets"), 10, 1, 60)] self.__make_grid(table, rows) return table def make_settings_page(self): self.__tests[TEST_TYPE_FIXEDMULTI] = self.make_fixedmulti_settings() self.__tests[TEST_TYPE_GRADMULTI] = self.make_gradmulti_settings() box = gtk.VBox(False, 0) for v in self.__tests.values(): box.pack_start(v, 1, 1, 1) self.__tests[TEST_TYPE_FIXEDMULTI].show() box.show() return box def make_stats_table(self): table = gtk.Table(3, 4) col = 0 row = 0 for i in ["", _("Sent"), _("Received"), _("Total")]: lab = gtk.Label(i) lab.show() table.attach(lab, col, col+1, 0, 1) col += 1 lab = gtk.Label(_("Packets")) lab.show() table.attach(lab, 0, 1, 1, 2) lab = gtk.Label(_("Bytes")) lab.show() table.attach(lab, 0, 1, 2, 3) self.__stats_vals = {} spec = [("ps", "pr", "pt"), ("bs", "br", "bt")] _row = 1 for row in spec: _col = 1 for col in row: lab = gtk.Label() lab.show() self.__stats_vals[col] = lab table.attach(lab, _col, _col+1, _row, _row+1) _col += 1 _row += 1 table.show() return table def make_test_page(self): vbox = gtk.VBox(False, 0) frame = gtk.Frame(_("Status")) self.__test_status = gtk.Entry() self.__test_status.show() self.__test_status.set_editable(False) frame.add(self.__test_status) frame.show() vbox.pack_start(frame, 0, 0, 0) frame = gtk.Frame(_("Statistics")) frame.add(self.make_stats_table()) frame.show() vbox.pack_start(frame, 1, 1, 1) hbox = gtk.HBox(False, 2) self.__loss = gtk.Label("") self.__loss.show() hbox.pack_start(self.__loss, 0, 0, 0) self.__prog = gtk.ProgressBar() self.__prog.set_fraction(0.0) self.__prog.show() hbox.pack_start(self.__prog, 1, 1, 1) hbox.show() vbox.pack_start(hbox) button = gtk.Button(_("Start")) button.connect("clicked", self.start_test) button.show() vbox.pack_start(button, 0, 0, 0) vbox.show() return vbox def set_test_val(self, *pairs): if len(pairs) % 2: print "Ack! need name=value pairs!" return for i in range(0, len(pairs), 2): name = pairs[i] val = pairs[i+1] self.__stats_vals[name].set_text("%i" % val) def set_test_status(self, status, frac, loss): self.__test_status.set_text(status) self.__prog.set_fraction(frac) self.__loss.set_text("%.1f %% copy" % (loss * 100.0)) def set_test_complete(self): self.set_page_complete(self.__test_page, True) def test_fixedmulti(self, station, port, size, packets): self.set_test_val("pt", packets, "bt", packets * size) class TestContext(object): def __init__(ctx): ctx.ps = ctx.pr = 0 ctx.cycle = 0 def update(ctx): self.set_test_val("ps", ctx.ps, "bs", ctx.ps * size) self.set_test_val("pr", ctx.pr, "br", ctx.pr * size) try: copy = ctx.pr / float(ctx.cycle) done = ctx.pr / float(packets) except ZeroDivisionError: return if ctx.complete(): self.set_test_complete() self.set_test_status("Complete", done, copy) else: self.set_test_status("Attempt %i of %i" % (ctx.ps, packets), done, copy) def complete(ctx): return ctx.cycle >= packets def sendping(ctx): ctx.ps += 1 data = "0" * int(size) gobject.timeout_add(calc_watchdog(size), ctx.timecb, ctx.ps) self.emit("ping-echo-station", station, port, data, ctx.recvcb, ctx.ps) def recvcb(ctx, number): if ctx.ps != number: return ctx.pr += 1 ctx.cycle += 1 if not ctx.complete() and self.enabled: ctx.sendping() ctx.update() def timecb(ctx, number): if ctx.ps != number: return ctx.cycle += 1 if not ctx.complete() and self.enabled: ctx.sendping() ctx.update() ctx = TestContext() ctx.sendping() ctx.update() def test_gradmulti(self, station, port, att, inc, start, end): ptotal = btotal = 0 sz = start while sz <= end: ptotal += att btotal += (att * sz) sz += inc self.set_test_val("pt", ptotal, "bt", btotal) class TestContext(object): def __init__(ctx): ctx.bs = ctx.br = ctx.ps = ctx.pr = 0 ctx.size = start ctx.cycle = 0 def update(ctx): self.set_test_val("ps", ctx.ps, "bs", ctx.bs) self.set_test_val("pr", ctx.pr, "br", ctx.br) done = ctx.br / float(btotal) copy = ctx.br / float(ctx.bs - ctx.size) if ctx.complete(): self.set_test_complete() self.set_test_status("Complete", done, copy) else: self.set_test_status("Attempt %i of %i at size %i" % (\ ((ctx.ps - 1) % att) + 1, att, ctx.size), done, copy) def complete(ctx): return ctx.cycle >= ptotal def sendping(ctx): if ctx.ps and (ctx.ps % att) == 0: ctx.size += inc ctx.bs += ctx.size ctx.ps += 1 data = "0" * int(ctx.size) gobject.timeout_add(calc_watchdog(ctx.size), ctx.timecb, ctx.ps) self.emit("ping-echo-station", station, data, ctx.recvb, ctx.ps) def recvb(ctx, number): if ctx.ps != number: return ctx.pr += 1 ctx.br += ctx.size ctx.cycle += 1 if not ctx.complete() and self.enabled: ctx.sendping() ctx.update() def timecb(ctx, number): if ctx.ps != number: return ctx.cycle += 1 if not ctx.complete() and self.enabled: ctx.sendping() ctx.update() ctx = TestContext() ctx.sendping() ctx.update() def start_test(self, button): button.set_sensitive(False) self.set_page_complete(self.__test_page, False) if self.__type == TEST_TYPE_FIXEDMULTI: self.test_fixedmulti(self.__station, self.__port, self.__values[_("Packet size")], self.__values[_("Number of packets")]) elif self.__type == TEST_TYPE_GRADMULTI: self.test_gradmulti(self.__station, self.__port, self.__values[_("Attempts per size")], self.__values[_("Increment size")], self.__values[_("Starting size")], self.__values[_("Ending size")]) def exit(self, foo, response): self.response = response self.enabled = False gtk.main_quit() def __init__(self, station="", port="DEFAULT"): baseclass.__init__(self) self.set_title("Connectivity Test") self.enabled = True self.__station = station self.__port = port self.__type = TEST_TYPE_FIXEDMULTI self.__tests = {} self.__values = {} self.__start_page = self.make_start_page(station, port) self.append_page(self.__start_page) self.set_page_title(self.__start_page, _("Test Type")) self.set_page_type(self.__start_page, gtk.ASSISTANT_PAGE_CONTENT) self.set_page_complete(self.__start_page, True) self.__settings_page = self.make_settings_page() self.append_page(self.__settings_page) self.set_page_title(self.__settings_page, _("Test Parameters")) self.set_page_type(self.__settings_page, gtk.ASSISTANT_PAGE_CONTENT) self.set_page_complete(self.__settings_page, True) self.__test_page = self.make_test_page() self.append_page(self.__test_page) self.set_page_title(self.__test_page, _("Run Test")) self.set_page_type(self.__test_page, gtk.ASSISTANT_PAGE_CONFIRM) self.set_page_complete(self.__test_page, False) self.connect("cancel", self.exit, gtk.RESPONSE_CANCEL) self.connect("apply", self.exit, gtk.RESPONSE_OK) def run(self): self.show() self.set_modal(True) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) gtk.main() self.hide() if __name__ == "__main__": a = ConnTestAssistant("KK7DS") a.show() gtk.main() d-rats-0.3.3/d_rats/ui/main_chat.py000066400000000000000000000647261160617671700171300ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import time import re from datetime import datetime import gobject import gtk import pango from d_rats.ui.main_common import MainWindowElement, MainWindowTab from d_rats.ui.main_common import ask_for_confirmation, display_error, \ set_toolbar_buttons from d_rats import inputdialog, utils from d_rats import qst from d_rats import signals from d_rats import spell class LoggedTextBuffer(gtk.TextBuffer): def __init__(self, logfile): gtk.TextBuffer.__init__(self) self.__logfile = file(logfile, "a", 0) def get_logfile(self): return self.__logfile.name def insert_with_tags_by_name(self, iter, text, *attrs): gtk.TextBuffer.insert_with_tags_by_name(self, iter, text, *attrs) self.__logfile.write(text) class ChatQM(MainWindowElement): __gsignals__ = { "user-sent-qm" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } def _send_qm(self, view, path, col): model = view.get_model() iter = model.get_iter(path) text = model.get(iter, 0)[0] self.emit("user-sent-qm", text) def _add_qm(self, button, store): d = inputdialog.TextInputDialog(title=_("Add Quick Message")) d.label.set_text(_("Enter text for the new quick message:")) r = d.run() if r == gtk.RESPONSE_OK: key = time.strftime("%Y%m%d%H%M%S") store.append((d.text.get_text(), key)) self._config.set("quick", key, d.text.get_text()) d.destroy() def _rem_qm(self, button, view): (store, iter) = view.get_selection().get_selected() if not iter: return if not ask_for_confirmation(_("Really delete?"), self._wtree.get_widget("mainwindow")): return key, = store.get(iter, 1) store.remove(iter) self._config.remove_option("quick", key) def _reorder_rows(self, model, path): for i in self._config.options("quick"): self._config.remove_option("quick", i) i = 0 iter = model.get_iter_first() while iter: msg, = model.get(iter, 0) print "Setting %i: %s" % (i, msg) self._config.set("quick", "msg_%i" % i, msg) iter = model.iter_next(iter) i += 1 def __init__(self, wtree, config): MainWindowElement.__init__(self, wtree, config, "chat") qm_add, qm_rem, qm_list = self._getw("qm_add", "qm_remove", "qm_list") store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) store.connect("row-deleted", self._reorder_rows) qm_list.set_model(store) qm_list.set_headers_visible(False) qm_list.set_reorderable(True) qm_list.connect("row-activated", self._send_qm) r = gtk.CellRendererText() col = gtk.TreeViewColumn("", r, text=0) qm_list.append_column(col) for key in sorted(self._config.options("quick")): store.append((self._config.get("quick", key), key)) qm_add.connect("clicked", self._add_qm, store) qm_rem.connect("clicked", self._rem_qm, qm_list) class ChatQST(MainWindowElement): __gsignals__ = { "qst-fired" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)), } def _send_qst(self, view, path, col): store = view.get_model() id = store[path][0] q, c = self._qsts[id] self._qsts[id] = (q, 0) def _toggle_qst(self, rend, path, store, enbcol, idcol, fcol): val = store[path][enbcol] = not store[path][enbcol] id = store[path][idcol] freq = store[path][fcol] self._config.set(id, "enabled", val) q, c = self._qsts[id] self._qsts[id] = q, self._remaining_for(freq) * 60 def _add_qst(self, button, view): d = qst.QSTEditDialog(self._config, "qst_%s" % time.strftime("%Y%m%d%H%M%S")) if d.run() == gtk.RESPONSE_OK: d.save() self.reconfigure() d.destroy() def _rem_qst(self, button, view): (model, iter) = view.get_selection().get_selected() if not iter: return if not ask_for_confirmation(_("Really delete?"), self._wtree.get_widget("mainwindow")): return ident, = model.get(iter, 0) self._config.remove_section(ident) self._store.remove(iter) def _edit_qst(self, button, view): (model, iter) = view.get_selection().get_selected() if not iter: return ident, = model.get(iter, 0) d = qst.QSTEditDialog(self._config, ident) if d.run() == gtk.RESPONSE_OK: d.save() self.reconfigure() d.destroy() def __init__(self, wtree, config): MainWindowElement.__init__(self, wtree, config, "chat") qst_add, qst_rem, qst_edit, qst_list = self._getw("qst_add", "qst_remove", "qst_edit", "qst_list") self._store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN) qst_list.set_model(self._store) qst_list.connect("row-activated", self._send_qst) def render_remaining(col, rend, model, iter): id, e = model.get(iter, 0, 5) try: q, c = self._qsts[id] except KeyError: e = None if not e: s = "" elif c > 90: s = "%i mins" % (c / 60) else: s = "%i sec" % c rend.set_property("text", s) typ = gtk.TreeViewColumn("Type", gtk.CellRendererText(), text=1) frq = gtk.TreeViewColumn("Freq", gtk.CellRendererText(), text=2) r = gtk.CellRendererProgress() cnt = gtk.TreeViewColumn("Remaining", r, value=3) cnt.set_cell_data_func(r, render_remaining) msg = gtk.TreeViewColumn("Content", gtk.CellRendererText(), text=4) r = gtk.CellRendererToggle() r.connect("toggled", self._toggle_qst, self._store, 5, 0, 2) enb = gtk.TreeViewColumn("On", r, active=5) qst_list.append_column(typ) qst_list.append_column(frq) qst_list.append_column(cnt) qst_list.append_column(enb) qst_list.append_column(msg) self._qsts = {} self.reconfigure() qst_add.connect("clicked", self._add_qst, qst_list) qst_rem.connect("clicked", self._rem_qst, qst_list) qst_edit.connect("clicked", self._edit_qst, qst_list) gobject.timeout_add(1000, self._tick) def _remaining_for(self, freq): if freq.startswith(":"): n_min = int(freq[1:]) c_min = datetime.now().minute cnt = n_min - c_min if n_min <= c_min: cnt += 60 else: cnt = int(freq) return cnt def _qst_fired(self, q, content): self.emit("qst-fired", content, q.raw) def _tick(self): iter = self._store.get_iter_first() while iter: i, t, f, p, c, e = self._store.get(iter, 0, 1, 2, 3, 4, 5) if e: q, cnt = self._qsts[i] cnt -= 1 if cnt <= 0: q.fire() cnt = self._remaining_for(f) * 60 self._qsts[i] = (q, cnt) else: cnt = 0 if f.startswith(":"): period = 3600 else: period = int(f) * 60 p = (float(cnt) / period) * 100.0 self._store.set(iter, 3, p) iter = self._store.iter_next(iter) return True def reconfigure(self): self._store.clear() qsts = [x for x in self._config.sections() if x.startswith("qst_")] for i in qsts: t = self._config.get(i, "type") c = self._config.get(i, "content") f = self._config.get(i, "freq") e = self._config.getboolean(i, "enabled") self._store.append((i, t, f, 0.0, c, e)) qc = qst.get_qst_class(t) if not qc: print "Error: unable to get QST class `%s'" % t continue q = qc(self._config, c) q.connect("qst-fired", self._qst_fired) self._qsts[i] = (q, self._remaining_for(f) * 60) class ChatTab(MainWindowTab): __gsignals__ = { "event" : signals.EVENT, "notice" : signals.NOTICE, "user-send-chat" : signals.USER_SEND_CHAT, } _signals = __gsignals__ def display_line(self, text, incoming, *attrs, **kwargs): """Display a single line of text with datestamp""" if (time.time() - self._last_date) > 600: stamp = time.strftime("%Y-%m-%d %H:%M:%S") else: stamp = time.strftime("%H:%M:%S") if self._config.getboolean("prefs", "chat_timestamp"): line = "[%s] %s" % (stamp, text) else: line = text self._last_date = time.time() self._display_line(line, incoming, "default", *attrs, **kwargs) def _highlight_tab(self, num): child = self.__filtertabs.get_nth_page(num) label = self.__filtertabs.get_tab_label(child) mkup = "%s" % label.get_text() label.set_markup(mkup) def _unhighlight_tab(self, num): child = self.__filtertabs.get_nth_page(num) label = self.__filtertabs.get_tab_label(child) label.set_markup(label.get_text()) def _display_matching_filter(self, text): for filter, display in self.__filters.items(): if filter and filter in text: return display return self.__filters[None] def _display_selected(self): cur = self.__filtertabs.get_current_page() return self.__filtertabs.get_nth_page(cur).child def _maybe_highlight_header(self, buffer, mark): start = buffer.get_iter_at_mark(mark) try: s, e = start.forward_search("] ", 0) except: return try: s, end = e.forward_search(": ", 0) except: return # If we get here, we saw '] FOO: ' so highlight between # the start and the end buffer.apply_tag_by_name("bold", start, end) def _display_for_channel(self, channel): if self.__filters.has_key(channel): return self.__filters[channel] else: return None def _display_line(self, text, apply_filters, *attrs, **kwargs): match = re.match("^([^#].*)(#[^/]+)//(.*)$", text) if "priv_src" in kwargs.keys(): channel = "@%s" % kwargs["priv_src"] display = self._display_for_channel(channel) if not display: print "Creating channel %s" % channel self._build_filter(channel) self._save_filters() display = self._display_for_channel(channel) elif match and apply_filters: channel = match.group(2) text = match.group(1) + match.group(3) display = self._display_for_channel(channel) elif apply_filters: display = self._display_matching_filter(text) noticere = self._config.get("prefs", "noticere") ignorere = self._config.get("prefs", "ignorere") if noticere and re.search(noticere, text): attrs += ("noticecolor",) elif ignorere and re.search(ignorere, text): attrs += ("ignorecolor",) else: display = self._display_selected() if not display: # We don't have anywhere to display this, so ignore it return buffer = display.get_buffer() sw = display.parent (start, end) = buffer.get_bounds() mark = buffer.create_mark(None, end, True) buffer.insert_with_tags_by_name(end, text + os.linesep, *attrs) self._maybe_highlight_header(buffer, mark) buffer.delete_mark(mark) adj = sw.get_vadjustment() bot_scrolled = (adj.get_value() == (adj.upper - adj.page_size)) endmark = buffer.get_mark("end") if bot_scrolled: display.scroll_to_mark(endmark, 0.0, True, 0, 1) tabnum = self.__filtertabs.page_num(display.parent) if tabnum != self.__filtertabs.get_current_page() and \ "ignorecolor" not in attrs: self._highlight_tab(tabnum) if apply_filters and "ignorecolor" not in attrs: self._notice() def _send_button(self, button, dest, entry): buffer = entry.get_buffer() text = buffer.get_text(*buffer.get_bounds()) if not text: return dcall = "CQCQCQ" num = self.__filtertabs.get_current_page() child = self.__filtertabs.get_nth_page(num) channel = self.__filtertabs.get_tab_label(child).get_text() if channel.startswith("#"): text = channel + "//" + text elif channel.startswith("@"): dcall = channel[1:] port = dest.get_active_text() buffer.delete(*buffer.get_bounds()) self.emit("user-send-chat", dcall, port, text, False) def _send_msg(self, qm, msg, raw, dest): port = dest.get_active_text() self.emit("user-send-chat", "CQCQCQ", port, msg, raw) def _bcast_file(self, but, dest): dir = self._config.get("prefs", "download_dir") fn = self._config.platform.gui_open_file(dir) if not fn: return try: f = file(fn) except Exception, e: display_error(_("Unable to open file %s: %s") % (fn, e)) return data = f.read() f.close() if len(data) > (2 << 12): display_error(_("File is too large to send (>8KB)")) return port = dest.get_active_text() self.emit("user-send-chat", "CQCQCQ", port, "\r\n" + data, False) def _clear(self, but): display = self._display_selected() display.get_buffer().set_text("") def _tab_selected(self, tabs, page, num): self._unhighlight_tab(num) delf = self._wtree.get_widget("main_menu_delfilter") delf.set_sensitive(num != 0) self.__tb_buttons[_("Remove Filter")].set_sensitive(num != 0) def _tab_reordered(self, tabs, page, num): self._save_filters() def _save_filters(self): rev = {} for key, val in self.__filters.items(): rev[val] = key filters = [] for i in range(0, self.__filtertabs.get_n_pages()): display = self.__filtertabs.get_nth_page(i).child if rev.get(display, None): filters.append(rev[display]) self._config.set("state", "filters", str(filters)) def _add_filter(self, but): d = inputdialog.TextInputDialog(title=_("Create filter")) d.label.set_text(_("Enter a filter search string:")) r = d.run() text = d.text.get_text() d.destroy() if not text: return if r == gtk.RESPONSE_OK: self._build_filter(text) self._save_filters() def _del_filter(self, but): idx = self.__filtertabs.get_current_page() page = self.__filtertabs.get_nth_page(idx) text = self.__filtertabs.get_tab_label(page).get_text() del self.__filters[text] self.__filtertabs.remove_page(idx) self._save_filters() def _view_log(self, but): display = self._display_selected() fn = display.get_buffer().get_logfile() self._config.platform.open_text_file(fn) def _enter_to_send(self, view, event, dest): if event.keyval == 65293: self._send_button(None, dest, view) return True elif event.keyval >= 65470 and event.keyval <= 65482: index = event.keyval - 65470 msgs = sorted(self._config.options("quick")) port = dest.get_active_text() if index < len(msgs): msg = self._config.get("quick", msgs[index]) self.emit("user-send-chat", "CQCQCQ", port, msg, False) def _join_channel(self, button): while True: d = inputdialog.TextInputDialog(title=_("Join Channel")) d.label.set_text(_("Enter channel name:")) r = d.run() text = d.text.get_text() d.destroy() if not text: return elif r != gtk.RESPONSE_OK: return if text.startswith("#"): text = text[1:] if re.match("^[A-z0-9_-]+$", text): self._build_filter("#" + text) self._save_filters() break display_error(_("Channel names must be a single-word " + "alphanumeric string")) def _query_user(self, button): while True: d = inputdialog.TextInputDialog(title=_("Query User")) d.label.set_text(_("Enter station:")) r = d.run() text = d.text.get_text() d.destroy() if not text: return elif r != gtk.RESPONSE_OK: return if text.startswith("@"): text = text[1:] if re.match("^[A-z0-9_-]+$", text): self._build_filter("@" + text.upper()) self._save_filters() break display_error(_("Station must be a plain " + "alphanumeric string")) def _init_toolbar(self): jnchannel = self._config.ship_img("chat-joinchannel.png") addfilter = self._config.ship_img("chat-addfilter.png") delfilter = self._config.ship_img("chat-delfilter.png") queryuser = self._config.ship_img("chat-query.png") tb, = self._getw("toolbar") set_toolbar_buttons(self._config, tb) buttons = \ [(addfilter, _("Add Filter"), self._add_filter), (delfilter, _("Remove Filter"), self._del_filter), (jnchannel, _("Join Channel"), self._join_channel), (queryuser, _("Open Private Chat"), self._query_user), ] c = 0 for i, l, f in buttons: icon = gtk.Image() icon.set_from_pixbuf(i) icon.show() item = gtk.ToolButton(icon, l) item.connect("clicked", f) try: item.set_tooltip_text(l) except AttributeError: pass item.show() tb.insert(item, c) self.__tb_buttons[l] = item c += 1 def __init__(self, wtree, config): MainWindowTab.__init__(self, wtree, config, "chat") entry, send, dest = self._getw("entry", "send", "destination") self.__filtertabs, = self._getw("filtertabs") self.__filters = {} self.__filtertabs.remove_page(0) self.__filtertabs.connect("switch-page", self._tab_selected) self.__filtertabs.connect("page-reordered", self._tab_reordered) self.__tb_buttons = {} addf = self._wtree.get_widget("main_menu_addfilter") addf.connect("activate", self._add_filter) delf = self._wtree.get_widget("main_menu_delfilter") delf.connect("activate", self._del_filter) vlog = self._wtree.get_widget("main_menu_viewlog") vlog.connect("activate", self._view_log) send.connect("clicked", self._send_button, dest, entry) send.set_flags(gtk.CAN_DEFAULT) send.connect("expose-event", lambda w, e: w.grab_default()) if self._config.getboolean("prefs", "check_spelling"): spell.prepare_TextBuffer(entry.get_buffer()) entry.set_wrap_mode(gtk.WRAP_WORD) entry.connect("key-press-event", self._enter_to_send, dest) entry.grab_focus() self._qm = ChatQM(wtree, config) self._qst = ChatQST(wtree, config) self._qm.connect("user-sent-qm", self._send_msg, False, dest) self._qst.connect("qst-fired", self._send_msg, dest) self._last_date = 0 bcast = self._wtree.get_widget("main_menu_bcast") bcast.connect("activate", self._bcast_file, dest) clear = self._wtree.get_widget("main_menu_clear") clear.connect("activate", self._clear) try: dest.set_tooltip_text(_("Choose the port where chat " + "and QST messages will be sent")) except AttributeError: # Old PyGTK doesn't have this pass self._init_toolbar() self.reconfigure() def _reconfigure_colors(self, buffer): tags = buffer.get_tag_table() if not tags.lookup("incomingcolor"): for color in ["red", "blue", "green", "grey"]: tag = gtk.TextTag(color) tag.set_property("foreground", color) tags.add(tag) tag = gtk.TextTag("bold") tag.set_property("weight", pango.WEIGHT_BOLD) tags.add(tag) tag = gtk.TextTag("italic") tag.set_property("style", pango.STYLE_ITALIC) tags.add(tag) tag = gtk.TextTag("default") tag.set_property("indent", -40) tag.set_property("indent-set", True) tags.add(tag) regular = ["incomingcolor", "outgoingcolor", "noticecolor", "ignorecolor"] reverse = ["brokencolor"] for i in regular + reverse: tag = tags.lookup(i) if not tag: tag = gtk.TextTag(i) tags.add(tag) if i in regular: tag.set_property("foreground", self._config.get("prefs", i)) elif i in reverse: tag.set_property("background", self._config.get("prefs", i)) def _reconfigure_font(self, display): fontname = self._config.get("prefs", "font") font = pango.FontDescription(fontname) display.modify_font(font) def _build_filter(self, text): if text is not None: ffn = self._config.platform.filter_filename(text) else: ffn = "Main" fn = self._config.platform.log_file(ffn) buffer = LoggedTextBuffer(fn) buffer.create_mark("end", buffer.get_end_iter(), False) display = gtk.TextView(buffer) display.set_wrap_mode(gtk.WRAP_WORD_CHAR) display.set_editable(False) display.set_cursor_visible(False) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.add(display) display.show() sw.show() if text: lab = gtk.Label(text) else: lab = gtk.Label(_("Main")) lab.show() self.__filtertabs.append_page(sw, lab) if text is not None: self.__filtertabs.set_tab_reorderable(sw, True) self.__filters[text] = display self._reconfigure_colors(buffer) self._reconfigure_font(display) def _configure_filters(self): for i in range(0, self.__filtertabs.get_n_pages()): self.__filtertabs.remove_page(i) self.__filters = {} filters = eval(self._config.get("state", "filters")) while None in filters: filters.remove(None) filters.insert(0, None) # Main catch-all for filter in filters: self._build_filter(filter) def reconfigure(self): if not self.__filters.has_key(None): # First time only self._configure_filters() for display in self.__filters.values(): self._reconfigure_colors(display.get_buffer()) self._reconfigure_font(display) dest, = self._getw("destination") ports = [] for p in self._config.options("ports"): spec = self._config.get("ports", p) vals = spec.split(",") if vals[0] == "True": ports.append(vals[-1]) ports.sort() model = dest.get_model() if model: model.clear() else: model = gtk.ListStore(gobject.TYPE_STRING) dest.set_model(model) for port in ports: model.append((port,)) if ports: utils.combo_select(dest, ports[0]) def get_selected_port(self): dest, = self._getw("destination") return dest.get_active_text() def selected(self): MainWindowTab.selected(self) make_visible = ["main_menu_bcast", "main_menu_clear", "main_menu_addfilter", "main_menu_delfilter", "main_menu_viewlog"] for name in make_visible: item = self._wtree.get_widget(name) item.set_property("visible", True) entry, = self._getw("entry") gobject.idle_add(entry.grab_focus) def deselected(self): MainWindowTab.deselected(self) make_invisible = ["main_menu_bcast", "main_menu_clear", "main_menu_addfilter", "main_menu_delfilter", "main_menu_viewlog"] for name in make_invisible: item = self._wtree.get_widget(name) item.set_property("visible", False) d-rats-0.3.3/d_rats/ui/main_common.py000066400000000000000000000113511160617671700174630ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import gobject import gtk from d_rats import inputdialog, miscwidgets from d_rats import signals STATION_REGEX = "^[A-Z0-9- /_]+$" def ask_for_confirmation(question, parent=None): d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=parent, message_format=question) r = d.run() d.destroy() return r == gtk.RESPONSE_YES def display_error(message, parent=None): d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=parent, message_format=message) r = d.run() d.destroy() return r == gtk.RESPONSE_OK def prompt_for_station(_station_list, config, parent=None): station_list = [str(x) for x in _station_list] port_list = [] for i in config.options("ports"): enb, port, rate, sniff, raw, name = config.get("ports", i).split(",") if enb == "True": port_list.append(name) defsta = defprt = "" if station_list: defsta = str(station_list[0]) if port_list: defprt = port_list[0] port_list.sort() station_list.sort() station = miscwidgets.make_choice(station_list, True, defsta) port = miscwidgets.make_choice(port_list, False, defprt) d = inputdialog.FieldDialog(title=_("Enter destination"), parent=parent) d.add_field(_("Station"), station) d.add_field(_("Port"), port) station.child.set_activates_default(True) while True: res = d.run() if res != gtk.RESPONSE_OK: break s = station.get_active_text().upper() if "@" in s: display_error(_("You must enter a station callsign. " + "You cannot use an email address here"), d) continue elif not re.match(STATION_REGEX, s): display_error(_("Invalid character in callsign"), d) continue break p = port.get_active_text() d.destroy() if res == gtk.RESPONSE_OK: return s, p else: return None, None def prompt_for_string(message, parent=None, orig=""): d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=parent, message_format=message) e = gtk.Entry() e.set_text(orig) e.show() d.vbox.pack_start(e, 1, 1, 1) r = d.run() d.destroy() if r == gtk.RESPONSE_OK: return e.get_text() else: return None def set_toolbar_buttons(config, tb): tbsize = config.get("prefs", "toolbar_button_size") if tbsize == _("Default"): tb.unset_style() tb.unset_icon_size() elif tbsize == _("Small"): tb.set_style(gtk.TOOLBAR_ICONS) tb.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR) elif tbsize == _("Large"): tb.set_style(gtk.TOOLBAR_BOTH) tb.set_icon_size(gtk.ICON_SIZE_LARGE_TOOLBAR) class MainWindowElement(gobject.GObject): def __init__(self, wtree, config, prefix): self._prefix = prefix self._wtree = wtree self._config = config gobject.GObject.__init__(self) def _getw(self, *names): widgets = [] for _name in names: name = "%s_%s" % (self._prefix, _name) widgets.append(self._wtree.get_widget(name)) return tuple(widgets) def reconfigure(self): pass class MainWindowTab(MainWindowElement): def __init__(self, wtree, config, prefix): MainWindowElement.__init__(self, wtree, config, prefix) self._tablabel = wtree.get_widget("tab_label_%s" % prefix) self._selected = False def reconfigure(self): pass def selected(self): self._selected = True self._unnotice() def deselected(self): self._selected = False def _notice(self): self.emit("notice") if self._selected: return text = self._tablabel.get_text() self._tablabel.set_markup("%s" % text) def _unnotice(self): text = self._tablabel.get_text() self._tablabel.set_markup(text) d-rats-0.3.3/d_rats/ui/main_events.py000066400000000000000000000303211160617671700174750ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time import os from datetime import datetime import gobject import gtk from d_rats.ui.main_common import MainWindowElement, MainWindowTab from d_rats import utils from d_rats import signals EVENT_INFO = 0 EVENT_FILE_XFER = 1 EVENT_FORM_XFER = 2 EVENT_PING = 3 EVENT_POS_REPORT = 4 EVENT_SESSION = 5 EVENT_GROUP_NONE = -1 _EVENT_TYPES = {EVENT_INFO : None, EVENT_FILE_XFER : None, EVENT_FORM_XFER : None, EVENT_PING : None, EVENT_POS_REPORT : None, EVENT_SESSION : None, } FILTER_HINT = _("Enter filter text") class Event(object): def __init__(self, group_id, message, evtype=EVENT_INFO): self._group_id = group_id if evtype not in _EVENT_TYPES.keys(): raise Exception("Invalid event type %i" % evtype) self._evtype = evtype self._message = message self._isfinal = False self._details = "" def set_as_final(self): "This event ends a series of events in the given group" self._isfinal = True def is_final(self): return self._isfinal def set_details(self, details): self._details = details class FileEvent(Event): def __init__(self, group_id, message): Event.__init__(self, group_id, message, EVENT_FILE_XFER) class FormEvent(Event): def __init__(self, group_id, message): Event.__init__(self, group_id, message, EVENT_FORM_XFER) class PingEvent(Event): def __init__(self, group_id, message): Event.__init__(self, group_id, message, EVENT_PING) class PosReportEvent(Event): def __init__(self, group_id, message): Event.__init__(self, group_id, message, EVENT_POS_REPORT) class SessionEvent(Event): def __init__(self, session_id, port_id, message): group_id = "%s_%s" % (session_id, port_id) message = "[%s] %s" % (port_id, message) Event.__init__(self, group_id, message, EVENT_SESSION) self.__portid = port_id self.__sessionid = session_id self.__restart_info = None def get_portid(self): return self.__portid def get_sessionid(self): return self.__sessionid def set_restart_info(self, restart_info): self.__restart_info = restart_info def get_restart_info(self): return self.__restart_info def filter_rows(model, iter, evtab): search = evtab._wtree.get_widget("event_searchtext").get_text() icon, message = model.get(iter, 1, 3) if search != FILTER_HINT: if search and message and search.upper() not in message.upper(): return False if evtab._filter_icon is None: return True else: return icon == evtab._filter_icon class EventTab(MainWindowTab): __gsignals__ = { "event" : signals.EVENT, "notice" : signals.NOTICE, "user-stop-session" : signals.USER_STOP_SESSION, "user-cancel-session" : signals.USER_CANCEL_SESSION, "user-send-file" : signals.USER_SEND_FILE, "status" : signals.STATUS, } _signals = __gsignals__ def _mh_xfer(self, _action, event): action = _action.get_name() sid = event.get_sessionid() portid = event.get_portid() if action == "stop": self.emit("user-stop-session", sid, portid) elif action == "cancel": self.emit("user-cancel-session", sid, portid) elif action == "restart": station, filename = event.get_restart_info() sname = os.path.basename(filename) self.emit("user-send-file", station, portid, filename, sname) event.set_restart_info(None) def _make_session_menu(self, sid, event): xml = """ """ ag = gtk.ActionGroup("menu") actions = [("stop", _("Stop"), not event.is_final()), ("cancel", _("Cancel"), not event.is_final()), ("restart", _("Restart"), event.get_restart_info())] for action, label, sensitive in actions: a = gtk.Action(action, label, None, None) a.connect("activate", self._mh_xfer, event) a.set_sensitive(bool(sensitive)) ag.add_action(a) uim = gtk.UIManager() uim.insert_action_group(ag, 0) uim.add_ui_from_string(xml) return uim.get_widget("/menu") def _mouse_cb(self, view, uievent): if uievent.button != 3: return if uievent.window == view.get_bin_window(): x, y = uievent.get_coords() pathinfo = view.get_path_at_pos(int(x), int(y)) if pathinfo is None: return else: view.set_cursor_on_cell(pathinfo[0]) (model, iter) = view.get_selection().get_selected() type, id, event = model.get(iter, 1, 0, 6) menus = { _EVENT_TYPES[EVENT_SESSION] : self._make_session_menu, } menufn = menus.get(type, None) if menufn: menu = menufn(id, event) menu.popup(None, None, None, uievent.button, uievent.time) def _type_selected(self, typesel, filtermodel): filter = typesel.get_active_text() print "Filter on %s" % filter if filter == _("All"): t = None elif filter == _("File Transfers"): t = EVENT_FILE_XFER elif filter == _("Form Transfers"): t = EVENT_FORM_XFER elif filter == _("Pings"): t = EVENT_PING elif filter == _("Position Reports"): t = EVENT_POS_REPORT if t is None: self._filter_icon = None else: self._filter_icon = _EVENT_TYPES[t] filtermodel.refilter() def _search_text(self, searchtext, filtermodel): filtermodel.refilter() def _load_pixbufs(self): _EVENT_TYPES[EVENT_INFO] = self._config.ship_img("event_info.png") _EVENT_TYPES[EVENT_FILE_XFER] = self._config.ship_img("folder.png") _EVENT_TYPES[EVENT_FORM_XFER] = self._config.ship_img("message.png") _EVENT_TYPES[EVENT_PING] = self._config.ship_img("event_ping.png") _EVENT_TYPES[EVENT_SESSION] = self._config.ship_img("event_session.png") _EVENT_TYPES[EVENT_POS_REPORT] = \ self._config.ship_img("event_posreport.png") def __change_sort(self, column): srt = column.get_sort_order() if srt == gtk.SORT_ASCENDING: srt = gtk.SORT_DESCENDING else: srt = gtk.SORT_ASCENDING self._config.set("state", "events_sort", int(srt)) self.store.set_sort_column_id(5, srt) column.set_sort_order(srt) def _get_sort_asc(self): srt = self._config.getint("state", "events_sort") return srt == gtk.SORT_ASCENDING def __init__(self, wtree, config): MainWindowTab.__init__(self, wtree, config, "event") self.__ctr = 0 eventlist, = self._getw("list") eventlist.connect("button_press_event", self._mouse_cb) self.store = gtk.ListStore(gobject.TYPE_STRING, # 0: id gobject.TYPE_OBJECT, # 1: icon gobject.TYPE_INT, # 2: timestamp gobject.TYPE_STRING, # 3: message gobject.TYPE_STRING, # 4: details gobject.TYPE_INT, # 5: order gobject.TYPE_PYOBJECT,# 6: event ) self._filter_icon = None filter = self.store.filter_new() filter.set_visible_func(filter_rows, self) eventlist.set_model(filter) col = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), pixbuf=1) eventlist.append_column(col) def render_time(col, rend, model, iter): val, = model.get(iter, 2) stamp = datetime.fromtimestamp(val) rend.set_property("text", stamp.strftime("%Y-%m-%d %H:%M:%S")) r = gtk.CellRendererText() col = gtk.TreeViewColumn(_("Time"), r, text=2) col.set_cell_data_func(r, render_time) col.set_sort_column_id(5) col.connect("clicked", self.__change_sort) eventlist.append_column(col) try: srt = int(self._config.get("state", "events_sort")) except ValueError: srt = gtk.SORT_DESCENDING self.store.set_sort_column_id(5, srt) col.set_sort_indicator(True) col.set_sort_order(srt) r = gtk.CellRendererText() col = gtk.TreeViewColumn(_("Description"), r, text=3) eventlist.append_column(col) typesel, = self._getw("typesel") typesel.set_active(0) typesel.connect("changed", self._type_selected, filter) filtertext, = self._getw("searchtext") filtertext.connect("changed", self._search_text, filter) utils.set_entry_hint(filtertext, FILTER_HINT) self._load_pixbufs() event = Event(None, _("D-RATS Started")) self.event(event) @utils.run_gtk_locked def _event(self, event): sw, = self._getw("sw") adj = sw.get_vadjustment() top_scrolled = (adj.get_value() == 0.0) bot_scrolled = (adj.get_value() == (adj.upper - adj.page_size)) if (adj.page_size == adj.upper) and self._get_sort_asc(): # This means we're top-sorted, but only because there aren't # enough items to have a scroll bar. So, if we're sorted # ascending, default to bottom-sort if we cross that boundary top_scrolled = False bot_scrolled = True iter = None if event._group_id != None: iter = self.store.get_iter_first() while iter: group, = self.store.get(iter, 0) if group == str(event._group_id): break iter = self.store.iter_next(iter) if not iter: iter = self.store.append() if event._isfinal: gid = "" else: gid = event._group_id or "" self.store.set(iter, 0, gid, 1, _EVENT_TYPES[event._evtype], 2, time.time(), 3, event._message, 4, event._details, 5, self.__ctr, 6, event) self.__ctr += 1 self._notice() self.emit("status", event._message) @utils.run_gtk_locked def top_scroll(adj): adj.set_value(0.0) @utils.run_gtk_locked def bot_scroll(adj): adj.set_value(adj.upper - adj.page_size) if top_scrolled: gobject.idle_add(top_scroll, adj) elif bot_scrolled: gobject.idle_add(bot_scroll, adj) def event(self, event): gobject.idle_add(self._event, event) def finalize_last(self, group): iter = self.store.get_iter_first() while iter: _group, = self.store.get(iter, 0) if _group == group: self.store.set(iter, 0, "") return True iter = self.store.iter_next(iter) return False def last_event_time(self, group): iter = self.store.get_iter_first() while iter: _group, stamp = self.store.get(iter, 0, 2) if _group == group: return stamp iter = self.store.iter_next(iter) return 0 d-rats-0.3.3/d_rats/ui/main_files.py000066400000000000000000000347161160617671700173070ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import time from glob import glob from datetime import datetime import gobject import gtk from d_rats.ui.main_common import MainWindowElement, MainWindowTab from d_rats.ui.main_common import ask_for_confirmation, set_toolbar_buttons from d_rats.sessions import rpc from d_rats.ui import main_events from d_rats import image from d_rats import utils from d_rats import signals from d_rats import inputdialog THROB_IMAGE = "throbber.gif" REMOTE_HINT = _("Enter remote callsign") class FileView(object): def __init__(self, view, path, config): self._view = view self._path = path self._store = gtk.ListStore(gobject.TYPE_OBJECT, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_INT) self._store.set_sort_column_id(1, gtk.SORT_ASCENDING) view.set_model(self._store) self._file_icon = config.ship_img("file.png") self.outstanding = {} def get_path(self): return self._path def set_path(self, path): self._path = path def refresh(self): pass def get_selected_filename(self): (model, iter) = self._view.get_selection().get_selected() if not iter: return None return model.get(iter, 1)[0] def add_explicit(self, name, size, stamp): self._store.append((self._file_icon, name, size, stamp)) def get_view(self): return self._view class LocalFileView(FileView): def refresh(self): self._store.clear() files = glob(os.path.join(self._path, "*")) for file in files: if os.path.isdir(file): continue print "Adding local file `%s'" % file try: stat = os.stat(file) ts = stat.st_mtime sz = stat.st_size nm = os.path.basename(file) self._store.append((self._file_icon, nm, sz, ts)) except Exception, e: print "Failed to add local file: %s" % e class RemoteFileView(FileView): def _file_list_cb(self, job, state, result): if state != "complete": print "Incomplete job" return unit_decoder = { "B" : 0, "KB": 10, "MB": 20 } # FIXME: This might need to be in the idle loop for k,v in result.items(): if "B (" in v: size, units, date, _time = v.split(" ") try: size = int(size) size <<= unit_decoder[units] stamp = "%s %s" % (date, _time) ts = time.mktime(time.strptime(stamp, "(%Y-%m-%d %H:%M:%S)")) except Exception, e: print "Unable to parse file info: %s" % e ts = time.time() size = 0 self._store.append((self._file_icon, k, size, ts)) else: self._store.append((self._file_icon, k, 0, 0)) def refresh(self): self._store.clear() job = rpc.RPCFileListJob(self.get_path(), "File list request") job.connect("state-change", self._file_list_cb) return job class FilesTab(MainWindowTab): __gsignals__ = { "event" : signals.EVENT, "notice" : signals.NOTICE, "submit-rpc-job" : signals.SUBMIT_RPC_JOB, "user-send-file" : signals.USER_SEND_FILE, "get-station-list" : signals.GET_STATION_LIST, "status" : signals.STATUS, } _signals = __gsignals__ def _emit(self, *args): gobject.idle_add(self.emit, *args) def _stop_throb(self): throbber, = self._getw("remote_throb") pix = self._config.ship_img(THROB_IMAGE) throbber.set_from_pixbuf(pix) def _end_list_job(self, job, state, *args): if not self._remote: return if self._remote.get_path() != job.get_dest(): return self._stop_throb() if state == "complete" and self._remote: self._remote.get_view().set_sensitive(True) self.emit("status", "Connected to %s" % job.get_dest()) else: self._disconnect(None, None) def _disconnect(self, button, rfview): if self._remote: view = self._remote.get_view() view.set_sensitive(False) view.get_model().clear() self._remote = None ssel, psel = self._get_ssel() ssel.set_sensitive(True) psel.set_sensitive(True) self._stop_throb() self.emit("status", "Disconnected") def _connect_remote(self, button, rfview): view, = self._getw("remote_list") ssel, psel = self._get_ssel() sta = ssel.get_active_text().upper() prt = psel.get_active_text() if not sta or sta.upper() == REMOTE_HINT.upper(): return if not self._remote or self._remote.get_path() != sta: self._remote = RemoteFileView(view, sta, self._config) throbber, = self._getw("remote_throb") img = self._config.ship_obj_fn(os.path.join("images", THROB_IMAGE)) anim = gtk.gdk.PixbufAnimation(img) throbber.set_from_animation(anim) job = self._remote.refresh() if job: self.emit("status", "Connecting to %s" % job.get_dest()) ssel.set_sensitive(False) psel.set_sensitive(False) job.connect("state-change", self._end_list_job) self.emit("submit-rpc-job", job, prt) def _refresh_local(self, *args): self._local.refresh() def refresh_local(self): self._notice() self._refresh_local() def _del(self, button, fileview): fname = self._local.get_selected_filename() if not fname: return question = _("Really delete %s?") % fname mainwin = self._wtree.get_widget("mainwindow") if not ask_for_confirmation(question, mainwin): return fn = os.path.join(self._config.get("prefs", "download_dir"), fname) os.remove(fn) self._local.refresh() def _upload(self, button, lfview): fname = self._local.get_selected_filename() if not fname: return fn = os.path.join(self._config.get("prefs", "download_dir"), fname) fnl = fn.lower() if fnl.endswith(".jpg") or \ fnl.endswith(".jpeg") or \ fnl.endswith(".png") or \ fnl.endswith(".gif"): fn = image.send_image(fn) if not fn: return ssel, psel = self._get_ssel() port = psel.get_active_text() if self._remote: station = self._remote.get_path() self._remote.outstanding[fname] = os.stat(fn).st_size else: station = ssel.get_active_text().upper() if not station or station.upper() == REMOTE_HINT.upper(): return self.emit("user-send-file", station, port, fn, fname) def _download(self, button, rfview): if not self._remote: return station = self._remote.get_path() fn = self._remote.get_selected_filename() ssel, psel = self._get_ssel() port = psel.get_active_text() def log_failure(job, state, result): rc = result.get("rc", "Timeout") if rc != "OK": event = main_events.Event(None, "%s: %s" % (job.get_dest(), rc)) self._emit("event", event) job = rpc.RPCPullFileJob(station, "Request file %s" % fn) job.connect("state-change", log_failure) job.set_file(fn) self.emit("submit-rpc-job", job, port) # FIXME: Need an event here def _delete(self, button, rfview): station = self._remote.get_path() d = inputdialog.TextInputDialog() d.label.set_text(_("Password for %s (blank if none):" % station)) d.text.set_visibility(False) if d.run() != gtk.RESPONSE_OK: return passwd = d.text.get_text() d.destroy() fn = self._remote.get_selected_filename() ssel, psel = self._get_ssel() port = psel.get_active_text() def log_failure(job, state, result): rc = result.get("rc", "Timeout") event = main_events.Event(None, "%s: %s" % (job.get_dest(), rc)) job = self._remote.refresh() self._emit("submit-rpc-job", job, port) self._emit("event", event) job = rpc.RPCDeleteFileJob(station, "Delete file %s" % fn) job.connect("state-change", log_failure) job.set_file(fn) job.set_pass(passwd) self.emit("submit-rpc-job", job, port) def _init_toolbar(self): def populate_tb(tb, buttons): c = 0 for i, l, f, d in buttons: icon = gtk.Image() icon.set_from_pixbuf(i) icon.show() item = gtk.ToolButton(icon, l) item.connect("clicked", f, d) try: item.set_tooltip_text(l) except AttributeError: pass item.show() tb.insert(item, c) c += 1 refresh = self._config.ship_img("files-refresh.png") connect = self._config.ship_img("connect.png") disconnect = self._config.ship_img("disconnect.png") delete = self._config.ship_img("msg-delete.png") dnload = self._config.ship_img("download.png") upload = self._config.ship_img("upload.png") ltb, = self._getw("local_toolbar") set_toolbar_buttons(self._config, ltb) lbuttons = \ [(refresh, _("Refresh"), self._refresh_local, self._local), (delete, _("Delete"), self._del, self._local), (upload, _("Upload"), self._upload, self._local), ] populate_tb(ltb, lbuttons) rtb, = self._getw("remote_toolbar") set_toolbar_buttons(self._config, rtb) rbuttons = \ [(connect, _("Connect"), self._connect_remote, self._remote), (disconnect, _("Disconnect"), self._disconnect, self._remote), (dnload, _("Download"), self._download, self._remote), (delete, _("Delete"), self._delete, self._remote), ] populate_tb(rtb, rbuttons) def _setup_file_view(self, view): def render_date(col, rend, model, iter): ts, = model.get(iter, 3) stamp = datetime.fromtimestamp(ts).strftime("%H:%M:%S %Y-%m-%d") rend.set_property("text", stamp) def render_size(col, rend, model, iter): sz, = model.get(iter, 2) if sz < 1024: s = "%i B" % sz else: s = "%.1f KB" % (sz / 1024.0) rend.set_property("text", s) col = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), pixbuf=0) view.append_column(col) col = gtk.TreeViewColumn(_("Filename"), gtk.CellRendererText(), text=1) col.set_sort_column_id(1) view.append_column(col) r = gtk.CellRendererText() col = gtk.TreeViewColumn(_("Size"), r, text=2) col.set_sort_column_id(2) col.set_cell_data_func(r, render_size) view.append_column(col) r = gtk.CellRendererText() col = gtk.TreeViewColumn(_("Date"), r, text=3) col.set_sort_column_id(2) col.set_cell_data_func(r, render_date) view.append_column(col) def _refresh_calls(self, stations, ports): activeport = ports.get_active() if activeport == -1: activeport = 0 stationlist = self.emit("get-station-list") _ports = [] _stations = [] sstore = stations.get_model() sstore.clear() pstore = ports.get_model() pstore.clear() if stationlist: for port, stations in stationlist.items(): _ports.append(port) for station in stations: _stations.append(str(station)) for station in sorted(_stations): sstore.append((station,)) for port in sorted(_ports): pstore.append((port,)) ports.set_active(activeport) return self.__selected def _get_ssel(self): return self._getw("sel_station", "sel_port") def __init__(self, wtree, config): MainWindowTab.__init__(self, wtree, config, "files") lview, rview = self._getw("local_list", "remote_list") self._setup_file_view(lview) self._setup_file_view(rview) stations, = self._getw("sel_station") utils.set_entry_hint(stations.child, REMOTE_HINT) ddir = self._config.get("prefs", "download_dir") self._local = LocalFileView(lview, None, self._config) self._remote = None rview.set_sensitive(False) self._init_toolbar() self._stop_throb() self.__selected = False self.reconfigure() def file_sent(self, _fn): fn = os.path.basename(_fn) if self._remote and self._remote.outstanding.has_key(fn): size = self._remote.outstanding[fn] del self._remote.outstanding[fn] self._remote.add_explicit(fn, size, time.time()) def reconfigure(self): self._local.set_path(self._config.get("prefs", "download_dir")) self._local.refresh() def selected(self): MainWindowTab.selected(self) self.__selected = True ssel, psel = self._get_ssel() self._refresh_calls(ssel, psel) gobject.timeout_add(1000, self._refresh_calls, ssel, psel) def deselected(self): MainWindowTab.deselected(self) self.__selected = False d-rats-0.3.3/d_rats/ui/main_messages.py000066400000000000000000001201231160617671700200000ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import time import shutil import random from datetime import datetime import gobject import gtk import pango from ConfigParser import ConfigParser,DuplicateSectionError from glob import glob from d_rats.ui.main_common import MainWindowElement, MainWindowTab from d_rats.ui.main_common import prompt_for_station, ask_for_confirmation, \ display_error, prompt_for_string, set_toolbar_buttons from d_rats.ui import main_events from d_rats import inputdialog from d_rats import formgui from d_rats import emailgw from d_rats.utils import log_exception, print_stack from d_rats import signals from d_rats import msgrouting from d_rats import wl2k _FOLDER_CACHE = {} BASE_FOLDERS = [_("Inbox"), _("Outbox"), _("Sent"), _("Trash"), _("Drafts")] def mkmsgid(callsign): r = random.SystemRandom().randint(0,100000) return "%s.%x.%x" % (callsign, int(time.time()) - 1114880400, r) class MessageFolderInfo(object): def __init__(self, folder_path): self._path = folder_path if _FOLDER_CACHE.has_key(folder_path): self._config = _FOLDER_CACHE[folder_path] else: self._config = ConfigParser() regpath = os.path.join(self._path, ".db") if os.path.exists(regpath): self._config.read(regpath) self._save() _FOLDER_CACHE[folder_path] = self._config def _save(self): regpath = os.path.join(self._path, ".db") f = file(regpath, "w") self._config.write(f) f.close() def name(self): """Return folder name""" return os.path.basename(self._path) def _setprop(self, filename, prop, value): filename = os.path.basename(filename) if not self._config.has_section(filename): self._config.add_section(filename) self._config.set(filename, prop, value) self._save() def _getprop(self, filename, prop): filename = os.path.basename(filename) try: return self._config.get(filename, prop) except Exception: return _("Unknown") def get_msg_subject(self, filename): return self._getprop(filename, "subject") def set_msg_subject(self, filename, subject): self._setprop(filename, "subject", subject) def get_msg_type(self, filename): return self._getprop(filename, "type") def set_msg_type(self, filename, type): self._setprop(filename, "type", type) def get_msg_read(self, filename): val = self._getprop(filename, "read") return val == "True" def set_msg_read(self, filename, read): self._setprop(filename, "read", str(read == True)) def get_msg_sender(self, filename): return self._getprop(filename, "sender") def set_msg_sender(self, filename, sender): self._setprop(filename, "sender", sender) def get_msg_recip(self, filename): return self._getprop(filename, "recip") def set_msg_recip(self, filename, recip): self._setprop(filename, "recip", recip) def subfolders(self): """Return a list of MessageFolderInfo objects representing this folder's subfolders""" info = [] entries = glob(os.path.join(self._path, "*")) for entry in sorted(entries): if entry == "." or entry == "..": continue if os.path.isdir(entry): info.append(MessageFolderInfo(entry)) return info def files(self): """Return a list of files contained in this folder""" l = glob(os.path.join(self._path, "*")) return [x for x in l if os.path.isfile(x) and not x.startswith(".")] def get_subfolder(self, name): """Get a MessageFolderInfo object representing a named subfolder""" for folder in self.subfolders(): if folder.name() == name: return folder return None def create_subfolder(self, name): """Create a subfolder by name""" path = os.path.join(self._path, name) os.mkdir(path) return MessageFolderInfo(path) def delete_self(self): try: os.remove(os.path.join(self._path, ".db")) except OSError: pass # Don't freak if no .db os.rmdir(self._path) def create_msg(self, name): exists = os.path.exists(os.path.join(self._path, name)) try: self._config.add_section(name) except DuplicateSectionError, e: if exists: raise e return os.path.join(self._path, name) def delete(self, filename): filename = os.path.basename(filename) self._config.remove_section(filename) os.remove(os.path.join(self._path, filename)) def rename(self, new_name): newpath = os.path.join(os.path.dirname(self._path), new_name) print "Renaming %s -> %s" % (self._path, newpath) os.rename(self._path, newpath) self._path = newpath def __str__(self): return self.name() class MessageFolders(MainWindowElement): __gsignals__ = { "user-selected-folder" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } def _folders_path(self): path = os.path.join(self._config.platform.config_dir(), "messages") if not os.path.isdir(path): os.makedirs(path) return path def _create_folder(self, root, name): info = root for el in name.split(os.sep)[:-1]: info = info.get_subfolder(el) if not info: break try: return info.create_subfolder(os.path.basename(name)) except Exception, e: raise Exception("Intermediate folder of %s does not exist" % name) def create_folder(self, name): root = MessageFolderInfo(self._folders_path()) return self._create_folder(root, name) def get_folders(self): return MessageFolderInfo(self._folders_path()).subfolders() def get_folder(self, name): return MessageFolderInfo(os.path.join(self._folders_path(), name)) def _get_folder_by_iter(self, store, iter): els = [] while iter: els.insert(0, store.get(iter, 0)[0]) iter = store.iter_parent(iter) return os.sep.join(els) def select_folder(self, folder): """Select a folder by path (i.e. Inbox/Subfolder) NB: Subfolders currently not supported :) """ view, = self._getw("folderlist") store = view.get_model() iter = store.get_iter_first() while iter: fqname = self._get_folder_by_iter(store, iter) if fqname == folder: view.set_cursor(store.get_path(iter)) self.emit("user-selected-folder", fqname) break iter = store.iter_next(iter) def _ensure_default_folders(self): root = MessageFolderInfo(self._folders_path()) for folder in BASE_FOLDERS: try: info = self._create_folder(root, folder) print info.subfolders() except Exception: pass def _add_folders(self, store, iter, root): iter = store.append(iter, (root.name(), self.folder_pixbuf)) for info in root.subfolders(): self._add_folders(store, iter, info) def _get_selected_folder(self, view, event): if event.window == view.get_bin_window(): x, y = event.get_coords() pathinfo = view.get_path_at_pos(int(x), int(y)) if pathinfo is None: return view.get_model(), None else: view.set_cursor_on_cell(pathinfo[0]) return view.get_selection().get_selected() def _mh(self, _action, store, iter, view): action = _action.get_name() if action == "delete": info = self.get_folder(self._get_folder_by_iter(store, iter)) try: info.delete_self() except OSError, e: display_error("Unable to delete folder: %s" % e) return store.remove(iter) elif action == "create": store.insert(iter, 0, ("New Folder", self.folder_pixbuf)) parent = self.get_folder(self._get_folder_by_iter(store, iter)) self._create_folder(parent, "New Folder") elif action == "rename": info = self.get_folder(self._get_folder_by_iter(store, iter)) new_text = prompt_for_string("Rename folder `%s' to:" % info.name(), orig=info.name()) if not new_text: return elif new_text == info.name(): return try: info.rename(new_text) except Exception, e: display_error("Unable to rename: %s" % e) return store.set(iter, 0, new_text) def _select_folder(self, view, event): store, iter = self._get_selected_folder(view, event) if not iter: return self.emit("user-selected-folder", self._get_folder_by_iter(store, iter)) def _move_cursor(self, view, step, count): try: (store, iter) = view.get_selection().get_selected() except Exception, e: print "Unable to find selected: %s" % e return self.emit("user-selected-folder", self._get_folder_by_iter(store, iter)) def _folder_menu(self, view, event): x = int(event.x) y = int(event.y) time = event.time pthinfo = view.get_path_at_pos(x, y) if pthinfo is not None: path, col, cellx, celly = pthinfo view.grab_focus() view.set_cursor(path, col, 0) xml = """ """ store, iter = self._get_selected_folder(view, event) folder = self._get_folder_by_iter(store, iter) can_del = bool(folder and (folder not in BASE_FOLDERS)) ag = gtk.ActionGroup("menu") actions = [("delete", _("Delete"), gtk.STOCK_DELETE, can_del), ("create", _("Create"), gtk.STOCK_NEW, True), ("rename", _("Rename"), None, can_del)] for action, label, stock, sensitive in actions: a = gtk.Action(action, label, None, stock) a.set_sensitive(sensitive) a.connect("activate", self._mh, store, iter, view) ag.add_action(a) uim = gtk.UIManager() uim.insert_action_group(ag, 0) uim.add_ui_from_string(xml) uim.get_widget("/menu").popup(None, None, None, event.button, event.time) def _mouse_cb(self, view, event): if event.button == 1: return self._select_folder(view, event) elif event.button == 3: return self._folder_menu(view, event) def _dragged_to(self, view, ctx, x, y, sel, info, ts): (path, place) = view.get_dest_row_at_pos(x, y) data = sel.data.split("\x01") msgs = data[1:] src_folder = data[0] dst_folder = self._get_folder_by_iter(view.get_model(), view.get_model().get_iter(path)) if src_folder == dst_folder: return dst = MessageFolderInfo(os.path.join(self._folders_path(), dst_folder)) src = MessageFolderInfo(os.path.join(self._folders_path(), src_folder)) for record in msgs: fn, subj, type, read, send, recp = record.split("\0") print "Dragged %s from %s into %s" % (fn, src_folder, dst_folder) print " %s %s %s %s->%s" % (subj, type, read, send, recp) try: dst.delete(os.path.basename(fn)) except Exception: pass newfn = dst.create_msg(os.path.basename(fn)) shutil.copy(fn, newfn) src.delete(fn) dst.set_msg_read(fn, read == "True") dst.set_msg_subject(fn, subj) dst.set_msg_type(fn, type) dst.set_msg_sender(fn, send) dst.set_msg_recip(fn, recp) def _folder_rename(self, render, path, new_text, store): iter = store.get_iter(path) orig = store.get(iter, 0)[0] if orig == new_text: return elif orig in BASE_FOLDERS: return info = self.get_folder(self._get_folder_by_iter(store, iter)) try: info.rename(new_text) except Exception, e: display_error("Unable to rename: %s" % e) return store.set(iter, 0, new_text) # MessageFolders def __init__(self, wtree, config): MainWindowElement.__init__(self, wtree, config, "msg") folderlist, = self._getw("folderlist") store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_OBJECT) folderlist.set_model(store) folderlist.set_headers_visible(False) folderlist.enable_model_drag_dest([("text/d-rats_message", 0, 0)], gtk.gdk.ACTION_DEFAULT) folderlist.connect("drag-data-received", self._dragged_to) folderlist.connect("button_press_event", self._mouse_cb) folderlist.connect_after("move-cursor", self._move_cursor) col = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), pixbuf=1) folderlist.append_column(col) rnd = gtk.CellRendererText() #rnd.set_property("editable", True) rnd.connect("edited", self._folder_rename, store) col = gtk.TreeViewColumn("", rnd, text=0) folderlist.append_column(col) self.folder_pixbuf = self._config.ship_img("folder.png") self._ensure_default_folders() for folder in self.get_folders(): self._add_folders(store, None, folder) ML_COL_ICON = 0 ML_COL_SEND = 1 ML_COL_SUBJ = 2 ML_COL_TYPE = 3 ML_COL_DATE = 4 ML_COL_FILE = 5 ML_COL_READ = 6 ML_COL_RECP = 7 class MessageList(MainWindowElement): __gsignals__ = {"prompt-send-form" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), "reply-form" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), "delete-form" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), } def _folder_path(self, folder): path = os.path.join(self._config.platform.config_dir(), "messages", folder) if not os.path.isdir(path): return None else: return path def open_msg(self, filename, editable, cb=None, cbdata=None): if not msgrouting.msg_lock(filename): display_error(_("Unable to open: message in use by another task")) return gtk.RESPONSE_CANCEL parent = self._wtree.get_widget("mainwindow") form = formgui.FormDialog(_("Form"), filename, parent=parent) form.configure(self._config) def form_done(dlg, response, info): saveable_actions = [formgui.RESPONSE_SAVE, formgui.RESPONSE_SEND, formgui.RESPONSE_SEND_VIA, ] dlg.hide() dlg.update_dst() msgrouting.msg_unlock(filename) if response in saveable_actions: print "Saving to %s" % filename dlg.save_to(filename) else: print "Not saving" dlg.destroy() self.refresh(filename) if cb: cb(response, cbdata) if response == formgui.RESPONSE_SEND: self.move_message(info, filename, _("Outbox")) elif response == formgui.RESPONSE_SEND_VIA: fn = self.move_message(info, filename, _("Outbox")) self.emit("prompt-send-form", fn) elif response == formgui.RESPONSE_REPLY: self.emit("reply-form", filename) elif response == formgui.RESPONSE_DELETE: self.emit("delete-form", filename) form.build_gui(editable) form.show() form.connect("response", form_done, self.current_info) def _open_msg(self, view, path, col): store = view.get_model() iter = store.get_iter(path) path, = store.get(iter, ML_COL_FILE) def close_msg_cb(response, info): if self.current_info == info: iter = self.iter_from_fn(path) print "Updating iter %s" % iter if iter: self._update_message_info(iter) else: print "Not current, not updating" editable = "Outbox" in path or "Drafts" in path # Dirty hack self.open_msg(path, editable, close_msg_cb, self.current_info) self.current_info.set_msg_read(path, True) iter = self.iter_from_fn(path) print "Updating iter %s" % iter if iter: self._update_message_info(iter) def _dragged_from(self, view, ctx, sel, info, ts): store, paths = view.get_selection().get_selected_rows() msgs = [os.path.dirname(store[paths[0]][ML_COL_FILE])] for path in paths: data = "%s\0%s\0%s\0%s\0%s\0%s" % (store[path][ML_COL_FILE], store[path][ML_COL_SUBJ], store[path][ML_COL_TYPE], store[path][ML_COL_READ], store[path][ML_COL_SEND], store[path][ML_COL_RECP]) msgs.append(data) sel.set("text/d-rats_message", 0, "\x01".join(msgs)) gobject.idle_add(self.refresh) # MessageList def __init__(self, wtree, config): MainWindowElement.__init__(self, wtree, config, "msg") msglist, = self._getw("msglist") self.store = gtk.ListStore(gobject.TYPE_OBJECT, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING) msglist.set_model(self.store) msglist.get_selection().set_mode(gtk.SELECTION_MULTIPLE) msglist.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [("text/d-rats_message", 0, 0)], gtk.gdk.ACTION_DEFAULT| gtk.gdk.ACTION_MOVE) msglist.connect("drag-data-get", self._dragged_from) col = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), pixbuf=0) msglist.append_column(col) def bold_if_unread(col, rend, model, iter, cnum): val, read, = model.get(iter, cnum, ML_COL_READ) if not val: val = "" if not read: val = val.replace("&", "&") val = val.replace("<", "<") val = val.replace(">", ">") rend.set_property("markup", "%s" % val) r = gtk.CellRendererText() r.set_property("ellipsize", pango.ELLIPSIZE_END) col = gtk.TreeViewColumn(_("Sender"), r, text=ML_COL_SEND) col.set_cell_data_func(r, bold_if_unread, ML_COL_SEND) col.set_sort_column_id(ML_COL_SEND) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) col.set_resizable(True) msglist.append_column(col) r = gtk.CellRendererText() r.set_property("ellipsize", pango.ELLIPSIZE_END) col = gtk.TreeViewColumn(_("Recipient"), r, text=ML_COL_RECP) col.set_cell_data_func(r, bold_if_unread, ML_COL_RECP) col.set_sort_column_id(ML_COL_RECP) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) col.set_resizable(True) msglist.append_column(col) r = gtk.CellRendererText() r.set_property("ellipsize", pango.ELLIPSIZE_END) col = gtk.TreeViewColumn(_("Subject"), r, text=ML_COL_SUBJ) col.set_cell_data_func(r, bold_if_unread, ML_COL_SUBJ) col.set_expand(True) col.set_sort_column_id(ML_COL_SUBJ) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) col.set_resizable(True) msglist.append_column(col) r = gtk.CellRendererText() r.set_property("ellipsize", pango.ELLIPSIZE_END) col = gtk.TreeViewColumn(_("Type"), r, text=ML_COL_TYPE) col.set_cell_data_func(r, bold_if_unread, ML_COL_TYPE) col.set_sort_column_id(ML_COL_TYPE) col.set_resizable(True) msglist.append_column(col) def render_date(col, rend, model, iter): ts, read = model.get(iter, ML_COL_DATE, ML_COL_READ) stamp = datetime.fromtimestamp(ts).strftime("%H:%M:%S %Y-%m-%d") if read: rend.set_property("text", stamp) else: rend.set_property("markup", "%s" % stamp) r = gtk.CellRendererText() r.set_property("ellipsize", pango.ELLIPSIZE_END) col = gtk.TreeViewColumn(_("Date"), r, text=ML_COL_DATE) col.set_cell_data_func(r, render_date) col.set_sort_column_id(ML_COL_DATE) col.set_resizable(True) msglist.append_column(col) msglist.connect("row-activated", self._open_msg) self.store.set_sort_column_id(ML_COL_DATE, gtk.SORT_DESCENDING) self.message_pixbuf = self._config.ship_img("message.png") self.unread_pixbuf = self._config.ship_img("msg-markunread.png") self.current_info = None def _update_message_info(self, iter, force=False): fn, = self.store.get(iter, ML_COL_FILE) subj = self.current_info.get_msg_subject(fn) read = self.current_info.get_msg_read(fn) if subj == _("Unknown") or force: # Not registered, so update the registry form = formgui.FormFile(fn) self.current_info.set_msg_type(fn, form.id) self.current_info.set_msg_read(fn, read) self.current_info.set_msg_subject(fn, form.get_subject_string()) self.current_info.set_msg_sender(fn, form.get_sender_string()) self.current_info.set_msg_recip(fn, form.get_recipient_string()) ts = os.stat(fn).st_ctime read = self.current_info.get_msg_read(fn) if read: icon = self.message_pixbuf else: icon = self.unread_pixbuf self.store.set(iter, ML_COL_ICON, icon, ML_COL_SEND, self.current_info.get_msg_sender(fn), ML_COL_RECP, self.current_info.get_msg_recip(fn), ML_COL_SUBJ, self.current_info.get_msg_subject(fn), ML_COL_TYPE, self.current_info.get_msg_type(fn), ML_COL_DATE, ts, ML_COL_READ, read) def iter_from_fn(self, fn): iter = self.store.get_iter_first() while iter: _fn, = self.store.get(iter, ML_COL_FILE) if _fn == fn: break iter = self.store.iter_next(iter) return iter def refresh(self, fn=None): """Refresh the current folder""" if fn is None: self.store.clear() for msg in self.current_info.files(): iter = self.store.append() self.store.set(iter, ML_COL_FILE, msg) self._update_message_info(iter) else: iter = self.iter_from_fn(fn) if not iter: iter = self.store.append() self.store.set(iter, ML_COL_FILE, fn) self._update_message_info(iter, True) def open_folder(self, path): """Open a folder by path""" self.current_info = MessageFolderInfo(self._folder_path(path)) self.refresh() def delete_selected_messages(self): msglist, = self._getw("msglist") iters = [] (store, paths) = msglist.get_selection().get_selected_rows() for path in paths: iters.append(store.get_iter(path)) for iter in iters: fn, = store.get(iter, ML_COL_FILE) store.remove(iter) self.current_info.delete(fn) def move_message(self, info, path, new_folder): dest = MessageFolderInfo(self._folder_path(new_folder)) try: newfn = dest.create_msg(os.path.basename(path)) except Exception: # Same folder, or duplicate message id return path print "Moving %s -> %s" % (path, newfn) shutil.copy(path, newfn) info.delete(path) if info == self.current_info: self.refresh() return newfn def move_selected_messages(self, folder): for msg in self.get_selected_messages(): self.move_message(self.current_info, msg, folder) def get_selected_messages(self): msglist, = self._getw("msglist") selected = [] (store, paths) = msglist.get_selection().get_selected_rows() for path in paths: selected.append(store[path][ML_COL_FILE]) return selected class MessagesTab(MainWindowTab): __gsignals__ = { "event" : signals.EVENT, "notice" : signals.NOTICE, "user-send-form" : signals.USER_SEND_FORM, "get-station-list" : signals.GET_STATION_LIST, "trigger-msg-router" : signals.TRIGGER_MSG_ROUTER, } _signals = __gsignals__ def _new_msg(self, button, msgtype=None): types = glob(os.path.join(self._config.form_source_dir(), "*.xml")) forms = {} for fn in types: forms[os.path.basename(fn).replace(".xml", "")] = fn if msgtype is None: parent = self._wtree.get_widget("mainwindow") d = inputdialog.ChoiceDialog(forms.keys(), title=_("Choose a form"), parent=parent) r = d.run() msgtype = d.choice.get_active_text() d.destroy() if r != gtk.RESPONSE_OK: return current = self._messages.current_info.name() self._folders.select_folder(_("Drafts")) tstamp = time.strftime("form_%m%d%Y_%H%M%S.xml") newfn = self._messages.current_info.create_msg(tstamp) form = formgui.FormFile(forms[msgtype]) call = self._config.get("user", "callsign") form.add_path_element(call) form.set_path_src(call) form.set_path_mid(mkmsgid(call)) form.save_to(newfn) def close_msg_cb(response, info): if response == int(gtk.RESPONSE_CLOSE): info.delete(newfn) if self._messages.current_info == info: self._messages.refresh() self._folders.select_folder(current) self._messages.open_msg(newfn, True, close_msg_cb, self._messages.current_info) def _rpl_msg(self, button, fn=None): def subj_reply(subj): if "RE:" in subj.upper(): return subj else: return "RE: %s" % subj def msg_reply(msg): if self._config.getboolean("prefs", "msg_include_reply"): return "--- Original Message ---\r\n\r\n" + msg else: return "" save_fields = [ ("_auto_number", "_auto_number", lambda x: str(int(x)+1)), ("_auto_subject", "_auto_subject", subj_reply), ("subject", "subject", lambda x: "RE: %s" % x), ("message", "message", msg_reply), ("_auto_sender", "_auto_recip", None), ] if not fn: try: sel = self._messages.get_selected_messages() except TypeError: return if len(sel) > 1: print "FIXME: Warn about multiple reply" return fn = sel[0] current = self._messages.current_info.name() self._folders.select_folder(_("Drafts")) oform = formgui.FormFile(fn) tmpl = os.path.join(self._config.form_source_dir(), "%s.xml" % oform.id) nform = formgui.FormFile(tmpl) nform.add_path_element(self._config.get("user", "callsign")) try: for sf, df, xf in save_fields: oldval = oform.get_field_value(sf) if not oldval: continue if xf: nform.set_field_value(df, xf(oldval)) else: nform.set_field_value(df, oldval) except Exception, e: log_exception() print "Failed to do reply: %s" % e return if ";" in oform.get_path_dst(): rpath = ";".join(reversed(oform.get_path()[:-1])) print "rpath: %s (%s)" % (rpath, oform.get_path()) nform.set_path_dst(rpath) else: nform.set_path_dst(oform.get_path_src()) call = self._config.get("user", "callsign") nform.set_path_src(call) nform.set_path_mid(mkmsgid(call)) tstamp = time.strftime("form_%m%d%Y_%H%M%S.xml") newfn = self._messages.current_info.create_msg(tstamp) nform.save_to(newfn) def close_msg_cb(response, info): if self._messages.current_info == info: print "Respone was %i (%i)" % (response, gtk.RESPONSE_CANCEL) if response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_CLOSE]: info.delete(newfn) self._folders.select_folder(current) else: self._messages.refresh(newfn) self._messages.open_msg(newfn, True, close_msg_cb, self._messages.current_info) def _del_msg(self, button, fn=None): if fn: try: os.remove(fn) except Exception, e: print "Unable to delete %s: %s" % (fn, e) self._messages.refresh() else: if self._messages.current_info.name() == _("Trash"): self._messages.delete_selected_messages() else: self._messages.move_selected_messages(_("Trash")) def _snd_msg(self, button, fn=None): if not fn: try: sel = self._messages.get_selected_messages() except TypeError: return if len(sel) > 1: print "FIXME: Warn about multiple send" return fn = sel[0] recip = self._messages.current_info.get_msg_recip(fn) if not msgrouting.msg_lock(fn): display_error(_("Unable to send: message in use by another task")) return stations = [] ports = self.emit("get-station-list") for slist in ports.values(): stations += slist if recip in stations: stations.remove(recip) stations.insert(0, recip) station, port = prompt_for_station(stations, self._config) if not station: msgrouting.msg_unlock(fn) return self.emit("user-send-form", station, port, fn, "foo") def _mrk_msg(self, button, read): try: sel = self._messages.get_selected_messages() except TypeError: return for fn in sel: self._messages.current_info.set_msg_read(fn, read) self._messages.refresh() def _importmsg(self, button): dir = self._config.get("prefs", "download_dir") fn = self._config.platform.gui_open_file(dir) if not fn: return dst = os.path.join(self._config.form_store_dir(), _("Inbox"), time.strftime("form_%m%d%Y_%H%M%S.xml")) shutil.copy(fn, dst) self.refresh_if_folder(_("Inbox")) def _exportmsg(self, button): try: sel = self._messages.get_selected_messages() except TypeError: return if len(sel) > 1: print "FIXME: Warn about multiple send" return elif len(sel) == 0: return fn = sel[0] dir = self._config.get("prefs", "download_dir") nfn = self._config.platform.gui_save_file(dir, "msg.xml") if not nfn: return shutil.copy(fn, nfn) def _sndrcv(self, button, account=""): self.emit("trigger-msg-router", account) def _make_sndrcv_menu(self): menu = gtk.Menu() mi = gtk.MenuItem("Outbox") try: mi.set_tooltip_text("Send messages in the Outbox") except AttributeError: pass mi.connect("activate", self._sndrcv) mi.show() menu.append(mi) mi = gtk.MenuItem("WL2K") try: mi.set_tooltip_text("Check Winlink messages") except AttributeError: pass mi.connect("activate", self._sndrcv, "@WL2K") mi.show() menu.append(mi) for section in self._config.options("incoming_email"): info = self._config.get("incoming_email", section).split(",") lab = "%s on %s" % (info[1], info[0]) mi = gtk.MenuItem(lab) try: mi.set_tooltip_text("Check for new mail on this account") except AttributeError: pass mi.connect("activate", self._sndrcv, section) mi.show() menu.append(mi) return menu def _make_new_menu(self): menu = gtk.Menu() td = self._config.form_source_dir() for i in sorted(glob(os.path.join(td, "*.xml"))): msgtype = os.path.basename(i).replace(".xml", "") label = msgtype.replace("_", " ") mi = gtk.MenuItem(label) try: mi.set_tooltip_text("Create a new %s form" % label) except AttributeError: pass mi.connect("activate", self._new_msg, msgtype) mi.show() menu.append(mi) return menu def _init_toolbar(self): tb, = self._getw("toolbar") set_toolbar_buttons(self._config, tb) read = lambda b: self._mrk_msg(b, True) unread = lambda b: self._mrk_msg(b, False) buttons = [("msg-new.png", _("New"), self._new_msg), ("msg-send-via.png", _("Forward"), self._snd_msg), ("msg-reply.png", _("Reply"), self._rpl_msg), ("msg-delete.png", _("Delete"), self._del_msg), ("msg-markread.png", _("Mark Read"), read), ("msg-markunread.png", _("Mark Unread"), unread), ("msg-sendreceive.png", _("Send/Receive"), self._sndrcv), ] tips = { _("New") : _("Create a new message for sending"), _("Forward") : _("Manually direct a message to another station"), _("Reply") : _("Reply to the currently selected message"), _("Delete") : _("Delete the currently selected message"), _("Mark Read") : _("Mark the currently selected message as read"), _("Mark Unread") : _("Mark the currently selected message as unread"), _("Send/Receive") : _("Send messages in the Outbox"), } menus = { "msg-new.png" : self._make_new_menu(), "msg-sendreceive.png" : self._make_sndrcv_menu(), } c = 0 for i, l, f in buttons: icon = gtk.Image() icon.set_from_pixbuf(self._config.ship_img(i)) icon.show() if menus.has_key(i): item = gtk.MenuToolButton(icon, l) item.set_menu(menus[i]) try: item.set_arrow_tooltip_text("%s %s %s" % (_("More"), l, _("Options"))) except AttributeError: pass else: item = gtk.ToolButton(icon, l) item.show() item.connect("clicked", f) if tips.has_key(l): try: item.set_tooltip_text(tips[l]) except AttributeError: pass tb.insert(item, c) c += 1 def __init__(self, wtree, config): MainWindowTab.__init__(self, wtree, config, "msg") self._init_toolbar() self._folders = MessageFolders(wtree, config) self._messages = MessageList(wtree, config) self._messages.connect("prompt-send-form", self._snd_msg) self._messages.connect("reply-form", self._rpl_msg) self._messages.connect("delete-form", self._del_msg) self._folders.connect("user-selected-folder", lambda x, y: self._messages.open_folder(y)) self._folders.select_folder(_("Inbox")) iport = self._wtree.get_widget("main_menu_importmsg") iport.connect("activate", self._importmsg) eport = self._wtree.get_widget("main_menu_exportmsg") eport.connect("activate", self._exportmsg); def refresh_if_folder(self, folder): self._notice() if self._messages.current_info.name() == folder: self._messages.refresh() def message_sent(self, fn): outbox = self._folders.get_folder(_("Outbox")) files = outbox.files() if fn in files: sent = self._folders.get_folder(_("Sent")) newfn = sent.create_msg(os.path.basename(fn)) print "Moving %s -> %s" % (fn, newfn) shutil.copy(fn, newfn) outbox.delete(fn) self.refresh_if_folder(_("Outbox")) self.refresh_if_folder(_("Sent")) else: print "Form %s sent but not in outbox" % os.path.basename(fn) def get_shared_messages(self, for_station): """Return a list of (title, stamp, filename) forms destined for station @for_station""" shared = _("Inbox") path = os.path.join(self._config.platform.config_dir(), "messages") if not os.path.isdir(path): os.makedirs(path) info = MessageFolderInfo(os.path.join(path, shared)) ret = [] for fn in info.files(): stamp = os.stat(fn).st_mtime ffn = "%s/%s" % (shared, os.path.basename(fn)) form = formgui.FormFile(fn) ret.append((form.get_subject_string(), stamp, ffn)) return ret def selected(self): MainWindowTab.selected(self) make_visible = ["main_menu_importmsg", "main_menu_exportmsg"] for name in make_visible: item = self._wtree.get_widget(name) item.set_property("visible", True) def deselected(self): MainWindowTab.deselected(self) make_invisible = ["main_menu_importmsg", "main_menu_exportmsg"] for name in make_invisible: item = self._wtree.get_widget(name) item.set_property("visible", False) d-rats-0.3.3/d_rats/ui/main_stations.py000066400000000000000000000402231160617671700200370ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2009 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import gtk import gobject import time import os from d_rats.ui.main_common import MainWindowTab from d_rats.ui import main_events from d_rats.ui import conntest from d_rats.sessions import rpc from d_rats import station_status from d_rats import signals from d_rats import image from d_rats import miscwidgets from d_rats import inputdialog from d_rats import utils def prompt_for_account(config): accounts = {} for section in config.options("incoming_email"): info = config.get("incoming_email", section).split(",") key = "%s on %s" % (info[1], info[0]) accounts[key] = info wl2k_call = config.get("user", "callsign") wl2k_ssid = config.get("prefs", "msg_wl2k_ssid").strip() if wl2k_ssid: wl2k_call = "%s-%s" % (wl2k_call, wl2k_ssid) accounts["Other"] = ["", "", "", "", "", "110"] accounts["WL2K"] = ["@WL2K", wl2k_call, "", "", "", "0"] default = accounts.keys()[0] account = miscwidgets.make_choice(accounts.keys(), False, default) host = gtk.Entry() user = gtk.Entry() pasw = gtk.Entry() ussl = gtk.CheckButton() port = gtk.SpinButton(gtk.Adjustment(110, 1, 65535, 1), digits=0) disable = [host, user, pasw, ussl, port] pasw.set_visibility(False) def choose_account(box): info = accounts[box.get_active_text()] for i in disable: i.set_sensitive(not info[0]) host.set_text(info[0]) user.set_text(info[1]) pasw.set_text(info[2]) ussl.set_active(info[4] == "True") port.set_value(int(info[5])) account.connect("changed", choose_account) choose_account(account) d = inputdialog.FieldDialog(title="Select account") d.add_field("Account", account) d.add_field("Server", host) d.add_field("Username", user) d.add_field("Password", pasw) d.add_field("Use SSL", ussl) d.add_field("Port", port) r = d.run() d.destroy() if r == gtk.RESPONSE_CANCEL: return None return host.get_text(), user.get_text(), pasw.get_text(), \ str(ussl.get_active()), str(int(port.get_value())) class StationsList(MainWindowTab): __gsignals__ = { "event" : signals.EVENT, "notice" : signals.NOTICE, "get-station-list" : signals.GET_STATION_LIST, "ping-station" : signals.PING_STATION, "ping-station-echo" : signals.PING_STATION_ECHO, "incoming-chat-message" : signals.INCOMING_CHAT_MESSAGE, "submit-rpc-job" : signals.SUBMIT_RPC_JOB, "user-send-file" : signals.USER_SEND_FILE, } _signals = __gsignals__ def _update(self): self.__view.queue_draw() return True def _mh(self, _action, station, port): action = _action.get_name() model = self.__view.get_model() iter = model.get_iter_first() while iter: _station, = model.get(iter, 0) if _station == station: break iter = model.iter_next(iter) if action == "ping": # FIXME: Use the port we saw the user on self.emit("ping-station", station, port) elif action == "conntest": ct = conntest.ConnTestAssistant(station, port) ct.connect("ping-echo-station", lambda a, *v: self.emit("ping-station-echo", *v)) ct.run() elif action == "remove": self.__calls.remove(station) self._update_station_count() model.remove(iter) elif action == "reset": model.set(iter, 1, time.time()) elif action == "reqpos": job = rpc.RPCPositionReport(station, "Position Request") def log_result(job, state, result): msg = result.get("rc", "(Error)") if msg != "OK": event = main_events.Event(None, "%s %s: %s" % (station, _("says"), msg)) self.emit("event", event) print "Result: %s" % str(result) job.set_station(station) job.connect("state-change", log_result) # FIXME: Send on the port where we saw this user self.emit("submit-rpc-job", job, port) elif action == "clearall": model.clear() self.__calls = [] self._update_station_count() elif action == "pingall": stationlist = self.emit("get-station-list") for port in stationlist.keys(): print "Doing CQCQCQ ping on port %s" % port self.emit("ping-station", "CQCQCQ", port) elif action == "reqposall": job = rpc.RPCPositionReport("CQCQCQ", "Position Request") job.set_station(".") stationlist = self.emit("get-station-list") for port in stationlist.keys(): self.emit("submit-rpc-job", job, port) elif action == "sendfile": fn = self._config.platform.gui_open_file() if not fn: return fnl = fn.lower() if fnl.endswith(".jpg") or fnl.endswith(".jpeg") or \ fnl.endswith(".png") or fnl.endswith(".gif"): fn = image.send_image(fn) if not fn: return name = os.path.basename(fn) self.emit("user-send-file", station, port, fn, name) elif action == "version": def log_result(job, state, result): if state == "complete": msg = "Station %s running D-RATS %s on %s" % (\ job.get_dest(), result.get("version", "Unknown"), result.get("os", "Unknown")) print "Station %s reports version info: %s" % (\ job.get_dest(), result) else: msg = "No version response from %s" % job.get_dest() event = main_events.Event(None, msg) self.emit("event", event) job = rpc.RPCGetVersion(station, "Version Request") job.connect("state-change", log_result) self.emit("submit-rpc-job", job, port) elif action == "mcheck": def log_result(job, state, result): msg = "Mail check via %s: %s" % (job.get_dest(), result.get("msg", "No response")) event = main_events.Event(None, msg) self.emit("event", event) vals = prompt_for_account(self._config) if vals is None: return job = rpc.RPCCheckMail(station, "Mail Check") job.set_account(vals[0], vals[1], vals[2], vals[4], vals[3]) job.connect("state-change", log_result) self.emit("submit-rpc-job", job, port) def _make_station_menu(self, station, port): xml = """ """ ag = gtk.ActionGroup("menu") actions = [("ping", _("Ping"), None), ("conntest", _("Test Connectivity"), None), ("reqpos", _("Request Position"), None), ("sendfile", _("Send file"), None), ("remove", _("Remove"), gtk.STOCK_DELETE), ("reset", _("Reset"), gtk.STOCK_JUMP_TO), ("version", _("Get version"), gtk.STOCK_ABOUT), ("mcheck", _("Request mail check"), None)] for action, label, stock in actions: a = gtk.Action(action, label, None, stock) a.connect("activate", self._mh, station, port) a.set_sensitive(station is not None) ag.add_action(a) actions = [("clearall", _("Clear All"), gtk.STOCK_CLEAR), ("pingall", _("Ping All Stations"), None), ("reqposall", _("Request all positions"), None)] for action, label, stock in actions: a = gtk.Action(action, label, None, stock) a.connect("activate", self._mh, station, port) ag.add_action(a) uim = gtk.UIManager() uim.insert_action_group(ag, 0) uim.add_ui_from_string(xml) return uim.get_widget("/menu") def _mouse_cb(self, view, event): if event.button != 3: return if event.window == view.get_bin_window(): x, y = event.get_coords() pathinfo = view.get_path_at_pos(int(x), int(y)) if pathinfo is None: station = None port = None else: view.set_cursor_on_cell(pathinfo[0]) (model, iter) = view.get_selection().get_selected() station, port = model.get(iter, 0, 5) menu = self._make_station_menu(station, port) menu.popup(None, None, None, event.button, event.time) def __init__(self, wtree, config): MainWindowTab.__init__(self, wtree, config, "main") frame, self.__view, = self._getw("stations_frame", "stations_view") store = gtk.ListStore(gobject.TYPE_STRING, # Station gobject.TYPE_INT, # Timestamp gobject.TYPE_STRING, # Message gobject.TYPE_INT, # Status gobject.TYPE_STRING, # Status message gobject.TYPE_STRING) # Port store.set_sort_column_id(1, gtk.SORT_DESCENDING) self.__view.set_model(store) try: self.__view.set_tooltip_column(2) except AttributeError: print "This version of GTK is old; disabling station tooltips" self.__view.connect("button_press_event", self._mouse_cb) def render_call(col, rend, model, iter): call, ts, status = model.get(iter, 0, 1, 3) sec = time.time() - ts hour = 3600 day = (hour*24) if sec < 60: msg = call elif sec < hour: msg = "%s (%im)" % (call, (sec / 60)) elif sec < day: msg = "%s (%ih %im)" % (call, sec / 3600, (sec % 3600) / 60) else: msg = "%s (%id %ih)" % (call, sec / day, (sec % day) / 3600) if status == station_status.STATUS_ONLINE: color = "blue" elif status == station_status.STATUS_UNATTENDED: color = "#CC9900" elif status == station_status.STATUS_OFFLINE: color = "grey" else: color = "black" rend.set_property("markup", "%s" % (color, msg)) r = gtk.CellRendererText() col = gtk.TreeViewColumn(_("Stations"), r, text=0) col.set_cell_data_func(r, render_call) self.__view.append_column(col) self.__calls = [] self._update_station_count() status, msg = self._getw("stations_status", "stations_smsg") try: status.set_tooltip_text(_("This is the state other stations will " + "see when requesting your status")) msg.set_tooltip_text(_("This is the message other stations will " + "see when requesting your status")) except AttributeError: pass def set_status(cb): self.__status = cb.get_active_text() self._config.set("state", "status_state", self.__status) def set_smsg(e): self.__smsg = e.get_text() self._config.set("state", "status_msg", self.__smsg) for s in sorted(station_status.get_status_msgs().values()): if s not in [_("Unknown"), _("Offline")]: status.append_text(s) status.connect("changed", set_status) msg.connect("changed", set_smsg) prev_status = self._config.get("state", "status_state") if not utils.combo_select(status, prev_status): utils.combo_select(status, station_status.get_status_msgs().values()[0]) msg.set_text(self._config.get("state", "status_msg")) set_status(status) set_smsg(msg) gobject.timeout_add(30000, self._update) def _update_station_count(self): hdr, = self._getw("stations_header") hdr.set_markup("Stations (%i)" % len(self.__calls)) def saw_station(self, station, port, status=0, smsg=""): status_changed = False if station == "CQCQCQ": return store = self.__view.get_model() ts = time.time() msg = "%s %s %s %s\r\n%s: %s" % \ (_("Station"), station, _("last seen at"), time.strftime("%X %x", time.localtime(ts)), _("Port"), port) status_val = station_status.get_status_msgs().get(status, "Unknown") if station not in self.__calls: if smsg: msg += "\r\nStatus: %s (%s)" % (status_val, smsg) self.__calls.append(station) store.append((station, ts, msg, status, smsg, port)) self.__view.queue_draw() status_changed = True self._update_station_count() else: iter = store.get_iter_first() while iter: call, _status, _smsg = store.get(iter, 0, 3, 4) if call == station: status_changed = (status and (_status != status) or \ (smsg and (_smsg != smsg))) if _status > 0 and status == 0: status = _status if not smsg: smsg = _smsg msg += "\r\nStatus: %s (%s)" % (status_val, smsg) store.set(iter, 1, ts, 2, msg, 3, status, 4, smsg, 5, port) break iter = store.iter_next(iter) if status_changed and status > 0 and \ self._config.getboolean("prefs", "chat_showstatus"): self.emit("incoming-chat-message", station, "CQCQCQ", "%s %s: %s (%s %s)" % (_("Now"), status_val, smsg, _("Port"), port)) def get_status(self): sval = station_status.get_status_vals()[self.__status] return sval, self.__smsg def get_stations(self): stations = [] store = self.__view.get_model() iter = store.get_iter_first() while iter: call, ts, port = store.get(iter, 0, 1, 5) station = station_status.Station(call) station.set_heard(ts) station.set_port(port) stations.append(station) iter = store.iter_next(iter) return stations d-rats-0.3.3/d_rats/utils.py000066400000000000000000000166471160617671700157270ustar00rootroot00000000000000# # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import os import tempfile import urllib import platform def open_icon_map(iconfn): import gtk if not os.path.exists(iconfn): print "Icon file %s not found" % iconfn return None try: return gtk.gdk.pixbuf_new_from_file(iconfn) except Exception, e: print "Error opening icon map %s: %s" % (iconfn, e) return None ICON_MAPS = None def init_icon_maps(): global ICON_MAPS ICON_MAPS = { "/" : open_icon_map(os.path.join(platform.get_platform().source_dir(), "images", "aprs_pri.png")), "\\": open_icon_map(os.path.join(platform.get_platform().source_dir(), "images", "aprs_sec.png")), } def hexprint(data): col = 0 line_sz = 8 csum = 0 lines = len(data) / line_sz if (len(data) % line_sz) != 0: lines += 1 data += "\x00" * ((lines * line_sz) - len(data)) for i in range(0, (len(data)/line_sz)): print "%03i: " % (i * line_sz), left = len(data) - (i * line_sz) if left < line_sz: limit = left else: limit = line_sz for j in range(0,limit): print "%02x " % ord(data[(i * line_sz) + j]), csum += ord(data[(i * line_sz) + j]) csum = csum & 0xFF print " ", for j in range(0,limit): char = data[(i * line_sz) + j] if ord(char) > 0x20 and ord(char) < 0x7E: print "%s" % char, else: print ".", print "" return csum def filter_to_ascii(string): c = '\x00' xlate = ([c] * 32) + \ [chr(x) for x in range(32,127)] + \ ([c] * 129) xlate[ord('\n')] = '\n' xlate[ord('\r')] = '\r' return str(string).translate("".join(xlate)).replace("\x00", "") def run_safe(f): def runner(*args, **kwargs): try: return f(*args, **kwargs) except Exception, e: print "<<<%s>>> %s" % (f, e) return None return runner def run_gtk_locked(f): import gtk def runner(*args, **kwargs): gtk.gdk.threads_enter() try: f(*args, **kwargs) except Exception, e: gtk.gdk.threads_leave() raise gtk.gdk.threads_leave() return runner def run_or_error(f): import gtk from d_rats.ui import main_common def runner(*args, **kwargs): try: f(*args, **kwargs) except Exception, e: log_exception() main_common.display_error(_("An error occurred: ") + str(e)) return runner def print_stack(): import traceback, sys traceback.print_stack(file=sys.stdout) def get_sub_image(iconmap, i, j, size=20): import gtk # Account for division lines (1px per icon) x = (i * size) + i + 1 y = (j * size) + j + 1 icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 1, 8, size, size) iconmap.copy_area(x, y, size, size, icon, 0, 0) return icon def get_icon_from_map(iconmap, symbol): index = ord(symbol) - ord("!") i = index % 16 j = index / 16 #print "Symbol `%s' is %i,%i" % (symbol, i, j) return get_sub_image(iconmap, i, j) def get_icon(key): if not key: return None if len(key) == 2: if key[0] == "/": set = "/" elif key[0] == "\\": set = "\\" else: print "Unknown APRS symbol table: %s" % key[0] return None key = key[1] elif len(key) == 1: set = "/" else: print "Unknown APRS symbol: `%s'" % key return None try: return get_icon_from_map(ICON_MAPS[set], key) except Exception, e: print "Error cutting icon %s: %s" % (key, e) return None class NetFile(file): def __init__(self, uri, mode="r", buffering=1): self.__fn = uri self.is_temp = False methods = ["http", "https", "ftp"] for method in methods: if uri.startswith("%s://" % method): self.is_temp = True tmpf = tempfile.NamedTemporaryFile() self.__fn = tmpf.name tmpf.close() print "Retrieving %s -> %s" % (uri, self.__fn) urllib.urlretrieve(uri, self.__fn) break file.__init__(self, self.__fn, mode, buffering) def close(self): file.close(self) if self.is_temp: os.remove(self.__fn) class ExternalHash(object): def __init__(self): self.hval = "" def update(self, val): import popen2 stdout, stdin = popen2.popen2("md5sum") stdin.write(val) stdin.close() self.hval = stdout.read() stdout.close() def digest(self): return self.hval.split()[0] def combo_select(box, value): store = box.get_model() iter = store.get_iter_first() while iter: if store.get(iter, 0)[0] == value: box.set_active_iter(iter) return True iter = store.iter_next(iter) return False def log_exception(): import traceback import sys print "-- Exception: --" traceback.print_exc(limit=30, file=sys.stdout) print "------" def set_entry_hint(entry, hint, default_focused=False): import gtk def focus(entry, event, direction): if direction == "out" and not entry.get_text(): entry.set_text(hint) c = gtk.gdk.color_parse("grey") elif direction == "in" and entry.get_text() == hint: entry.set_text("") c = gtk.gdk.color_parse("black") else: return entry.modify_text(gtk.STATE_NORMAL, c) entry.connect("focus-in-event", focus, "in") entry.connect("focus-out-event", focus, "out") if not default_focused: focus(entry, None, "out") def port_for_station(ports, station): for port, stations in ports.items(): if station in stations: return port return None def make_error_dialog(msg, stack, buttons, type, extra): import gtk d = gtk.MessageDialog(buttons=buttons, type=type) if extra: extra(d) dvbox = gtk.VBox(False, 3) sv = gtk.TextView() sv.get_buffer().set_text(stack) dvbox.pack_start(sv, 1, 1, 1) sv.show() se = gtk.Expander(_("Details")) se.add(dvbox) dvbox.show() d.vbox.pack_start(se, 1, 1, 1) se.show() d.set_markup(msg) r = d.run() d.destroy() return r def dict_rev(target_dict, key): reverse = {} for k,v in target_dict.items(): reverse[v] = k print "Reversed dict: %s" % reverse return reverse[key] d-rats-0.3.3/d_rats/version.py000066400000000000000000000001111160617671700162270ustar00rootroot00000000000000DRATS_VERSION = "0.3.3" if __name__ == "__main__": print DRATS_VERSION d-rats-0.3.3/d_rats/wl2k.py000066400000000000000000000401041160617671700154270ustar00rootroot00000000000000import sys import os import socket import tempfile import subprocess import shutil import email import threading import gobject import struct import time import re sys.path.insert(0, "..") if __name__=="__main__": import gettext gettext.install("D-RATS") from d_rats import version from d_rats import platform from d_rats import formgui from d_rats import utils from d_rats import signals from d_rats.ddt2 import calc_checksum from d_rats.ui import main_events from d_rats import agw FBB_BLOCK_HDR = 1 FBB_BLOCK_DAT = 2 FBB_BLOCK_EOF = 4 FBB_BLOCK_TYPES = { FBB_BLOCK_HDR : "header", FBB_BLOCK_DAT : "data", FBB_BLOCK_EOF : "eof", } def escaped(string): return string.replace("\n", r"\n").replace("\r", r"\r") def run_lzhuf(cmd, data): p = platform.get_platform() cwd = tempfile.mkdtemp() f = file(os.path.join(cwd, "input"), "wb") f.write(data) f.close() kwargs = {} if subprocess.mswindows: su = subprocess.STARTUPINFO() su.dwFlags |= subprocess.STARTF_USESHOWWINDOW su.wShowWindow = subprocess.SW_HIDE kwargs["startupinfo"] = su if os.name == "nt": lzhuf = "LZHUF_1.EXE" elif os.name == "darwin": raise Exception("Not supported on MacOS") else: lzhuf = "lzhuf" lzhuf_path = os.path.abspath(os.path.join(p.source_dir(), "libexec", lzhuf)) shutil.copy(os.path.abspath(lzhuf_path), cwd) run = [lzhuf_path, cmd, "input", "output"] print "Running %s in %s" % (run, cwd) ret = subprocess.call(run, cwd=cwd, **kwargs) print "LZHUF returned %s" % ret if ret: return None f = file(os.path.join(cwd, "output"), "rb") data = f.read() f.close() return data def run_lzhuf_decode(data): return run_lzhuf("d", data[2:]) def run_lzhuf_encode(data): lzh = run_lzhuf("e", data) lzh = struct.pack(" %s" % string self._conn.send(string + "\r") def __recv(self): resp = "" while not resp.endswith("\r"): resp += self._conn.recv(1) print " <- %s" % escaped(resp) return resp def _recv(self): r = ";" while r.startswith(";"): r = self.__recv() return r; def _send_ssid(self, recv_ssid): try: sw, ver, caps = recv_ssid[1:-1].split("-") except Exception: raise Exception("Conversation error (unparsable SSID `%s')" % resp) self._send(self.__ssid()) prompt = self._recv().strip() if not prompt.endswith(">"): raise Exception("Conversation error (never got prompt)") def __get_list(self): self._send("FF") msgs = [] reading = True while reading: resp = self._recv() for l in resp.split("\r"): if l.startswith("FC"): print "Creating message for %s" % l msgs.append(WinLinkMessage(l)) elif l.startswith("F>"): reading = False break elif l.startswith("FQ"): reading = False break elif not l: pass else: print "Invalid line: %s" % l raise Exception("Conversation error (%s while listing)" % l) return msgs def get_messages(self): self._connect() self._login() self.__messages = self.__get_list() if self.__messages: self._send("FS %s" % ("Y" * len(self.__messages))) for msg in self.__messages: print "Getting message..." try: msg.read_from_socket(self._conn) except Exception, e: raise #print e self._send("FQ") self._disconnect() return len(self.__messages) def get_message(self, index): return self.__messages[index] def send_messages(self, messages): if len(messages) != 1: raise Exception("Sorry, batch not implemented yet") self._connect() self._login() cs = 0 for msg in messages: p = msg.get_proposal() for i in p: cs += ord(i) cs += ord("\r") self._send(p) cs = ((~cs & 0xFF) + 1) self._send("F> %02X" % cs) resp = self._recv() if not resp.startswith("FS"): raise Exception("Error talking to server: %s" % resp) fs, accepts = resp.split() if len(accepts) != len(messages): raise Exception("Server refused some of my messages?!") for msg in messages: msg.send_to_socket(self._conn) resp = self._recv() self._disconnect() return 1 class WinLinkTelnet(WinLinkCMS): def __init__(self, callsign, server="server.winlink.org", port=8772): self.__server = server self.__port = port WinLinkCMS.__init__(self, callsign) def _connect(self): class sock_file: def __init__(self): self.__s = 0 def read(self, len): return self.__s.recv(len) def write(self, buf): return self.__s.send(buf) def connect(self, spec): return self.__s.connect(spec) def close(self): self.__s.close() self._conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._conn.connect((self.__server, self.__port)) def _disconnect(self): self._conn.close() def _login(self): resp = self._recv() resp = self._recv() if not resp.startswith("Callsign :"): raise Exception("Conversation error (never saw login)") self._send(self._callsign) resp = self._recv() if not resp.startswith("Password :"): raise Exception("Conversation error (never saw password)") self._send("CMSTELNET") resp = self._recv() self._send_ssid(resp) class WinLinkRMSPacket(WinLinkCMS): def __init__(self, callsign, remote, agw): self.__remote = remote self.__agw = agw WinLinkCMS.__init__(self, callsign) def _connect(self): self._conn = agw.AGW_AX25_Connection(self.__agw, self._callsign) self._conn.connect(self.__remote) def _disconnect(self): self._conn.disconnect() def _login(self): resp = self._recv() self._send_ssid(resp) class WinLinkThread(threading.Thread, gobject.GObject): __gsignals__ = { "mail-thread-complete" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN, gobject.TYPE_STRING)), "event" : signals.EVENT, "form-received" : signals.FORM_RECEIVED, "form-sent" : signals.FORM_SENT, } _signals = __gsignals__ def _emit(self, *args): gobject.idle_add(self.emit, *args) def __init__(self, config, callsign, callssid=None, send_msgs=[]): threading.Thread.__init__(self) self.setDaemon(True) gobject.GObject.__init__(self) if not callssid: callssid = callsign self._config = config self._callsign = callsign self._callssid = callssid self.__send_msgs = send_msgs def __create_form(self, msg): mail = email.message_from_string(msg.get_content()) sender = mail.get("From", "Unknown") if ":" in sender: method, sender = sender.split(":", 1) sender = "WL2K:" + sender if self._callsign == self._config.get("user", "callsign"): box = "Inbox" else: box = "Outbox" template = os.path.join(self._config.form_source_dir(), "email.xml") formfn = os.path.join(self._config.form_store_dir(), box, "%s.xml" % msg.get_id()) form = formgui.FormFile(template) form.set_field_value("_auto_sender", sender) form.set_field_value("recipient", self._callsign) form.set_field_value("subject", mail.get("Subject", "Unknown")) form.set_field_value("message", mail.get_payload()) form.set_path_src(sender.strip()) form.set_path_dst(self._callsign) form.set_path_mid(msg.get_id()) form.add_path_element("@WL2K") form.add_path_element(self._config.get("user", "callsign")) form.save_to(formfn) return formfn def _run_incoming(self): wl = self.wl2k_connect() count = wl.get_messages() for i in range(0, count): msg = wl.get_message(i) formfn = self.__create_form(msg) self._emit("form-received", -999, formfn) if count: result = "Queued %i messages" % count else: result = "No messages" return result def _run_outgoing(self): server = self._config.get("prefs", "msg_wl2k_server") port = self._config.getint("prefs", "msg_wl2k_port") wl = self.wl2k_connect() for mt in self.__send_msgs: m = re.search("Mid: (.*)\r\nSubject: (.*)\r\n", mt) if m: mid = m.groups()[0] subj = m.groups()[1] else: mid = time.strftime("%H%M%SDRATS") subj = "Message" wlm = WinLinkMessage() wlm.set_id(mid) wlm.set_content(mt, subj) print m print mt wl.send_messages([wlm]) #self._emit("form-sent", -999, return "Complete" def run(self): if self.__send_msgs: result = self._run_outgoing() else: result = self._run_incoming() self._emit("mail-thread-complete", True, result) class WinLinkTelnetThread(WinLinkThread): def __init__(self, *args, **kwargs): WinLinkThread.__init__(self, *args, **kwargs) def wl2k_connect(self): server = self._config.get("prefs", "msg_wl2k_server") port = self._config.getint("prefs", "msg_wl2k_port") return WinLinkTelnet(self._callssid, server, port) class WinLinkAGWThread(WinLinkThread): def __init__(self, *args, **kwargs): WinLinkThread.__init__(self, *args, **kwargs) self.__agwconn = None def set_agw_conn(self, agwconn): self.__agwconn = agwconn def wl2k_connect(self): remote = self._config.get("prefs", "msg_wl2k_rmscall") return WinLinkRMSPacket(self._callssid, remote, self.__agwconn) def wl2k_auto_thread(ma, *args, **kwargs): mode = ma.config.get("settings", "msg_wl2k_mode") #May need for AGW #call = config.get("user", "callsign") print "WL2K Mode is: %s" % mode if mode == "Network": mt = WinLinkTelnetThread(ma.config, *args, **kwargs) elif mode == "RMS": # TEMPORARY port = ma.config.get("prefs", "msg_wl2k_rmsport") if not ma.sm.has_key(port): raise Exception("No such AGW port %s for WL2K" % port) a = ma.sm[port][0].pipe.get_agw_connection() #a = agw.AGWConnection("127.0.0.1", 8000, 0.5) mt = WinLinkAGWThread(ma.config, *args, **kwargs) mt.set_agw_conn(a) else: raise Exception("Unknown WL2K mode: %s" % mode) return mt if __name__=="__main__": if True: #wl = WinLinkTelnet("KK7DS", "sandiego.winlink.org") agwc = agw.AGWConnection("127.0.0.1", 8000, 0.5) wl = WinLinkRMSPacket("KK7DS", "N7AAM-11", agwc) count = wl.get_messages() print "%i messages" % count for i in range(0, count): print "--Message %i--\n%s\n--End--\n\n" % (i, wl.get_message(i).get_content()) else: text = "This is a test!" _m = """Mid: 12345_KK7DS\r From: KK7DS\r To: dsmith@danplanet.com\r Subject: This is a test\r Body: %i\r \r %s """ % (len(text), text) m = WinLinkMessage() m.set_id("1234_KK7DS") m.set_content(_m) wl = WinLinkTelnet("KK7DS") wl.send_messages([m]) d-rats-0.3.3/d_rats/wu.py000066400000000000000000000056771160617671700152230ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import libxml2 import urllib class InvalidXMLError(Exception): pass WEATHER_KEYS = [ "temperature_string", "temp_f", "temp_c", "relative_humidity", "wind_string", "wind_dir", "wind_degrees", "wind_mph", "wind_gust_mph", "pressure_string", "pressure_mb", "pressure_in", "dewpoint_in", "dewpoint_f", "dewpoint_c", "heat_index_string", "heat_index_f", "heat_index_c", "windchill_string", "windchill_f", "windchill_c", "precip_1hr_string", "precip_1hr_in", "precip_1hr_metric", "precip_today_string", "precip_today_in", "precip_today_metric", ] class WUObservation(object): def __init__(self): self.location = {} self.station_id = None self.time = None self.weather = {} def __str__(self): return "%s: %s (%s)" % (self.location["full"], self.weather["temperature_string"], self.time) def __parse_location(self, node): child = node.children while child: self.location[child.name] = child.getContent() child = child.next def __parse_doc(self, doc): root = doc.children if root.name != "current_observation": raise InvalidXMLError("Root is not current_observation") child = root.children while child: if child.name in ["location", "observation_location"]: self.__parse_location(child) elif child.name == "station_id": self.staton_id = child.getContent() elif child.name == "observation_time_rfc822": try: self.time = datetime.datetime.strptime(\ child.getContent(), "%a, %d %B %Y %H:%M:%S %Z") except: self.time = child.getContent() elif child.name in WEATHER_KEYS: self.weather[child.name] = child.getContent() child = child.next def from_xml(self, xml): doc = libxml2.parseMemory(xml, len(xml)) return self.__parse_doc(doc) def from_uri(self, uri): fn, foo = urllib.urlretrieve(uri) doc = libxml2.parseFile(fn) os.remove(fn) return self.__parse_doc(doc) d-rats-0.3.3/d_rats/yencode.py000077500000000000000000000031361160617671700162050ustar00rootroot00000000000000#!/usr/bin/python # # Copyright 2008 Dan Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys DEFAULT_BANNED = "\x11\x13\x1A\00\xFD\xFE\xFF" OFFSET = 64 def yencode_buffer(buf, banned=None): if not banned: banned = DEFAULT_BANNED banned += "=" out = "" for char in buf: if char in banned: out += "=" + chr((ord(char) + OFFSET) % 256) else: out += char return out def ydecode_buffer(buf): out = "" i = 0 while i < len(buf): char = buf[i] if char == "=": i += 1 v = ord(buf[i]) - OFFSET if v < 0: v += 256 out += chr(v) else: out += char i += 1 return out if __name__=="__main__": import sys f = file(sys.argv[2]) inbuf = f.read() if sys.argv[1] == "-e": sys.stdout.write(yencode_buffer(inbuf)) else: sys.stdout.write(ydecode_buffer(inbuf)) f.close() d-rats-0.3.3/forms/000077500000000000000000000000001160617671700140515ustar00rootroot00000000000000d-rats-0.3.3/forms/ICS213_OR_WashCoARES.xml000066400000000000000000000020021160617671700177500ustar00rootroot00000000000000
ICS-213 Form Number 1 Precedence Routine Welfare Priority Emergency Originating station Originating location Time Date Recipient Sender Subject Message
d-rats-0.3.3/forms/ICS213_OR_WashCoARES.xsl000066400000000000000000000112031160617671700177610ustar00rootroot00000000000000

Date/Time of message
GENERAL MESSAGE
ICS 213-OS
Date/Time of reply
GENERAL MESSAGE June 2000 ICS213-OS
Electronic version: Generated by D-RATS
d-rats-0.3.3/forms/ICS213_US_OS.xml000066400000000000000000000021571160617671700164540ustar00rootroot00000000000000
ICS-213 Form Incident Name Time Date To ICS Position Sender ICS Position Subject Message Reply Signature / Position (person replying) Date of reply Time of reply
d-rats-0.3.3/forms/ICS213_US_OS.xsl000066400000000000000000000112571160617671700164630ustar00rootroot00000000000000

Date/Time of message
GENERAL MESSAGE
ICS 213-OS
Date/Time of reply
GENERAL MESSAGE June 2000 ICS213-OS
Electronic version: Generated by D-RATS
d-rats-0.3.3/forms/default.xsl000066400000000000000000000054631160617671700162350ustar00rootroot00000000000000

From: To:
2 1 label field
&nbsp;
d-rats-0.3.3/forms/email.xml000066400000000000000000000003451160617671700156640ustar00rootroot00000000000000
Email Message Subject Message
d-rats-0.3.3/forms/email.xsl000066400000000000000000000037331160617671700156760ustar00rootroot00000000000000
From:
Subject:
	    
	  
d-rats-0.3.3/forms/hics260.xml000066400000000000000000000126371160617671700157620ustar00rootroot00000000000000
HICS 260 - Patient Evacuation Tracking Form Date Unit Patient Name Age 0 MR# Diagnosis Admitting Physician Family Notified False Family Contact Info Accompanying Equipment Hospital BedIV PumpsIsolette/WarmerFoley CatheterGurneyOxygenTractionHalo-DeviceWheel ChairVentilatorMonitorCranial Bolt/ScrewAmbulatoryChest Tube(s)A-Line/SwanIO Device Isolation False Isolation Type Isolation Reason Departing Location Departure Room # Departure Time ID Band Confirmed False Confirmed By Medical Record Sent False Addressograph Sent False Belongings NoneWith PatientLeft in Room Valuables NoneWith PatientLeft in Safe Medications With PatientLeft on UnitTo Pharmacy Bag/Mask w/Tubing Sent False Bulb Syringe Sent False Arriving Location Arrival Room # Arrival Time ID Band Confirmed False Confirmed By Medical Record Sent False Addressograph False Belongings Received False Valuables Received False Medications Received False Bag/Mask w/Tubing Received False Bulb Syringe Received False Transferring to Another Facility Time to Staging Area Time Departing to Receiving Facility Destination Transportation ID Band Confirmed False Confirmed by Departure Time Facility Name
d-rats-0.3.3/forms/hics260.xsl000066400000000000000000000242251160617671700157640ustar00rootroot00000000000000
9.ACCOMPANYING EQUIPMENT
10. DEPARTING LOCATION
11. ARRIVING LOCATION
PEDS/INFANTS
12. TRANSFERRING TO ANOTHER FACILITY
Electronic version: Generated by D-RATS
YES NO YES NO
bigelement element
No
d-rats-0.3.3/forms/memo.xml000066400000000000000000000012451160617671700155320ustar00rootroot00000000000000
Informal Memo Form Time Date Recipient Sender Subject Message
d-rats-0.3.3/forms/radiogram.xml000066400000000000000000000032311160617671700165370ustar00rootroot00000000000000
Radiogram Number Precedence Routine Welfare Priority Emergency HX Station of Origin Check Place of Origin Time Date To Recipient Phone # Message Signature Received From Received Time Received Date Sent To Sent Time Sent Date
d-rats-0.3.3/forms/radiogram.xsl000066400000000000000000000133261160617671700165530ustar00rootroot00000000000000
To


Telephone Number
This radio message was received at Amateur Station Name Street Address City, State, ZIP



&nbsp;
d-rats-0.3.3/images/000077500000000000000000000000001160617671700141705ustar00rootroot00000000000000d-rats-0.3.3/images/aprs_pri.png000066400000000000000000000145651160617671700165300ustar00rootroot00000000000000‰PNG  IHDRQay‚sRGBЎЮщPLTEџџџџџїїџџџїџяџџїїџїїїяїџяїїџџœїяџоїџяяїяяяцяїцяяояџяцїццяцццоцїоцяоццжцїџџœџџооїцооооцооожояжоцНцџжооожцХоїЮжїЮжяжжжХжїЮжоХжяЮжжжЮцХжцжЮоХжоХжжжЮЮХжЮЮЮжЮЮЮХЮЮЕЮцХХХЮНЮЕХцœЮїЕХоНХХЅХї­ХоХНХЕХНННХННН­НоЕНХЅНоНЕНЅЕїЕЕХЕ­цЕЕЕœЕїЅЕо­­цœЕоЅ­яœЕжЅЕНЅ­цŒЕя”Ежџœ1­­­Ѕ­­”­жŒ­жЅЅЕЅЅ­Ю””ЅЅЅœЅЉŒЅЮ„ЅЮœœœœœ”Ѕ”Ѕ{œЮ„”оœ””””Ѕ{”ц”””„Œо„Œжs”Юs”ХŒŒœk”ХŒŒ”ŒŒŒНџR”їR”яkŒХџcŒ„„cŒХ„„”cŒН„„Œ„„„c„Х{sо{sж{sЮssтs„„s„{ssж„{{Z„Нk„Œ{{„B„я{{{„s„skкЮs{{œџs{sR{Нws{ss„wssssk{k{gswkskНœc1sksJsЕskkkksBsЕBkоkkkkkccksckkџџBkЕZkkgck:kЕRZЮcccsZckZcZcZcZkcZcџœcZZ1c­œZZkZZcZZZ)c­JJжJJЮRZZ:Jж)Z­JZRZRJJZJRRZZNR!Z­RRR!ZЅRRJR:ХJRRJ:Ю„B:жRJRJ:НB:Юџ!RЅRJJRJB::ж::ЮRЅ::ХяRBBJЅJBJJBBBBJоBBB:BBХ!:B:ЮB:BB::::Bc1c:::::11:B:1:):111:111)1:1)1!111))))1)))!)))!)!!!1œџоЮ !œ„{‰\tRNS@циfbKGDˆH pHYsУУЧoЈdtIMEи /KƒщzсIDATxкэ}Д\WUРЯП]ЌаїRЏ@ ИVЃб%E/VA@‚eБŠ€‚) <0Е(+N #ъе*уЕˆbЦjЉ P?žy˜кХ<Є/bгЇ–тhчАуЗ™—Л=пgŸ}Ю§š7I^ъьysяЭЭћё›НЯйgŸsі0s™Љ0`йž;wюф;lгћМˆ‚ZќќI"ЗmwЂљЄю VWёЮ ‘ЪDvfYСегв”ямyђ—ЛЏ§ІЛNi9ЙГєB.‘sЎЯ–Јg№„шdRїA‹ˆšГ№ abЭєP‡ДќBr#Э “D%ХgьU["КОО^ЭimЭ§[nIЪPЈAR­OЋЋ)&ЪЕ|’3DєбQїuж"jЫћEЉЃЗ3іfKtq&DС'ІˆЎ9 eD‘nъ-GдiYбеU„д#ЊšЈЄj6мЧБ…4вQEєфгПSу ˆž>]ATА\__W+(аЧДHѕF)Qє`šhю—ƒy1бU"хDY„(ПjТХмGЂКUDџbЗ5њшщRЂыО ЂDšЄ![FдPBD3‚?%gZO uдЊЇй№ЫбQІ T[Ž' B –ЇŸh*45Ÿд:КћwмqЧGЃV?-QˆD’ЅUU{Ёі`аˆzKu,'Њ_Ъ№э ВЕЕЬћђGљIMЖQ Ьвœ3CтЎž‚WТ)Ђw}п•ŒЛЄЗќ}œшiѕёKY}H”DkRˆйДХщкQ†4•ЃJGQ{‚LЗ6Ќ‰ufЯ*ˆŠm]’RЋwuНWбЛoTuхИЧS1 TMбпнёь+ПуШ‘#ї!=Ѕ’ЈЗЄюЏŽЈг{ОЯj~бЩdЋD™yWUf_Jд˜{ Žш}WНєфn&šKWн=+Ђ:”eD5‰NбiЌ^ЙЁЖnjfѕкдЭRQGOxјwАЛNнv —›пfˆьлчоPBTљік{Тu}Ђ‹іZ§DU5SжДfЮГ@ЇЊ™h]Ÿ€ИuаD Q:z3еќЕЯ6DїљSјЃ‰ДBtK5гVН'†НЇЩLМ'Хвˆ=_q’ЛїGN)ъfібS'gF4QLgч=mеУgа™xјœ%hžb­­ўОХ•ŠОю‹Џ~ю1Ћ_(#jkyMŘш=ќ­ЖB•’Г‰OДY+ЙЃКФ@(ЉXЫ"Rr3Л‘ЛЂG^ЪnЙvЧЕЗKЂ DJлLО;Zь=ЭЄК•H RђЩt‘ъсg™$)„Џ™!zъu;Ў|юЗьИъ'O9rђTSЂВ>R:zA"%3š‡§{ы*OдЌaQ:ЁЗпјнЏzлн[ˆ”\Шhоvсџа"ožЧ№ЗИs‘эк+2—џП=w—ШЮ9’ѓBtˆФKZЦjg‰н™ Б;;HьNп'а;—‘и"‰јŽЖЩЕАРнz+Є‡Ѓ[ I*dФ_ЕрI9[Jt8$HЕС†v› T"T"F-Нsy™ д5Й шrf PBtX[ЩвŠ#Я\!хІ~Q‡вЌEт7@ Є@ Є@ ЄEžыђ2E Јi @р_Ђ&BиœЈдОQ#]#%ЋC”D™ч+kЂ&ухюЙ|cЂцу>QЏбЋ‰ъ}„Јкщˆ‚пˆнLРр\Ÿ{>ЬЋˆЂЦŽ ‚{}2Dе?ёЉOxХoиeQwщQ6%QV›Ј9в#ъЕїASР…Љ‰2@5AŸŒ§Ьё%)Ч›{гтx№S|јЋ Q%:А+bѕ„Ј„XН?b[}H/-QлŽеDEdSш)$О’q У™ЙыD|Я­\Ўя“КўЇоїNAє-/~WЂШ--Зz[;…оS@/ЫˆF­ХѕŸHЂƒХ№9аN6Ь:)HK6-oPц\DTБ4D{ЦnнфВD‰x№žЧюЙџ­/{{=яipЁˆЂЉˆ с*Њ‰ ž”зLiG1ебuЎ šЇX—}Уп~т“'N{ЮK^Ћыеі4оSŒh#ЋxOЁеsУСp ў­ЃнL\3уDе†бQ]ᘭ"Ђ|oПBIХкxQDwјСO=јРпєИ-mз&к‰yј“jЂ5=|TŠn>UH78QЉЄЈЕTY9кjI’BјкУ{„мд?zг5Џy ђ№wПўРW<сЉOŒДB]kє"ДB#DkЕBQƒtˆˆf’hцˆF=|Эœяэ“с њыo•’fЂЮ? йH‰жQ§8Ђ\EQЁЄЕˆqVщ‘ЗJc?”-ТОлc0š'ˆŠЂT‰&к%Rк™I[ Gžэ+9Ž ђyФyУП$‰Ю…жї1™їмЭ­ўБE4'2ЋK#ЁГш‘_"ТfvKW\]8Ђ†єŸИžо9жтŸ3Э—Ыyzaˆв1g{,б=fЇчMYЂЂ'-ЉF›%#оРŽ`д~б^$>б%*P žЉщ*Щ“Ю “фЄздŸхnmLC”Žƒ=zHх4јЖ#b‹Sн’˜wR5*“ѕ5t/&JОdETтєˆr 9wЮsbeg бБљš˜>-рi5Fм‰ћОŽ Оэ\БkзЎi‰r%хљ[Н*ЦћЉёѕ8Иь….'ЊеsЌ˜a’Ы !_Ѓ ЕH_1QёчИAЕtЎSžŽюš^G-бDъhQPMРDMЉƒ‰vHY„ˆZ•вч*ЁцЎiШЖ`˜З !Q}‚nб!­;ђL (8ЄEMгxЭФЊt4Љ ŠуХ†ЈЛЖ#JяШи'{Юeeђ|щZЫ(ƒс2ъуЋeѕŽhdC‚і2™ЇЃ‚C*оKТ>–ќ№I\GЃD™еЯ @ˆ\еHQ~ѕtјŸ%JCŽЈЋђ=ЂЪ№Q”П["иBDq]ъЈGдCZ<—ЬеHеЮ%U),MO4’‰ЮdC@RMTМ%uЄеMЛaˆ.ЫЯВ# ЁGV‡(Гe‰„ fYђLQ8МwяaўHuˆКћ’џhD|ЂрLЇ#ЪЦ:lfUЌУсЙs0ьŸ2ьж,G™Ћ™ЄнKЄtŠЕ;SŽО•ЃмgIщ Ju]Њ jПcDtcUЛNхVН‡њ '_ѕЌž†,KЫQ‰Дm€Ъs?‚шЙsšзѕШЁЅ1Тu=" ‡“УPZ3ХtTеMЖ-Љ{§~OTѕb %х(+&–Ѓт^вvŠFU€дMTЙTЕУAкШ5VЏEЈ[ŽRЂгдѕ4мэyO–Ќ=ЛgŠZ CMњSЗ™мX™Fm&S3љ:'ъ ебЄš(IзL1ДшЏнйгnZ+8Я­аHЛ^{Oт_зѓы›ы(гЎSТІ"ŠкLvдј­аё8lз‹ŠdиѕГ/5г­ЯcЄЄvьЉбаwŒ^=JД`*НEJ˜m2•Н бМz;›EѓŠˆ§qСфТ†їц g-›Gœч1ќyЯн\ц=wsЋП8;гtKD Ц]OЦъіˆеМ}\ЏžgNAл.G ZМ#[­ру›Rі ˆКd~ћŠЁAн*Ѕ=bЌћёXo œПNнf“‰~„tчх$щMбM+ŽRби˜7ћа„(ˆ™i#Је#И‰Qіc7fl\‡Ј?5СьуžќAъzFЂЙфЈ^4%КЙщ#WЯ JOrУЉM,Ф' іUЃGŒ ‰Гуaxуf›žЃяOД<ш_КUнyhž“” „шц&A*‰RЄ€:pHfRУ, бЏдтˆЖїъ8._їЪй—(Yъ5w8‰N&Еˆ$YIEWGJ­ž/Фк}ŸYFSёјЃ&ёь@T†ю u”ГЇH Їk›W+uGЎэMV6'хЧй#Ч&1”Ю ѕj;ДžŽvЛ“IwТК]/д@КqˆдОzmˆJœJLзќІOtгЭФ-Ѕ”Ј7RРЗzц7Х'FQИŒ ЂэФОк­v;ЁDїХˆ yд;–‘&.yїi TG{т.бЎ$Њ:УJ‰Š `ЙH$фв(ЖˆPЂR_бЬ7№'‚?ƒ•E‰2J.уHљщЈ7žІнВD/+$ъe†‚ -YЦ6P;ЅDm†Еsч AкЬI*Ђ^<ёВк4Šц…ˆђЫш%j*cЂDjяЈQцХО ˆ^І”дmД5NОе*зQF‰‚E*nxУжžЉ!šбQб9aћ$вH9%*’yчбhЭ$е3 Њ”4‹-ВњZD~LД-8j‰ѕЪQcюЮъmxYн0D5PЈбб€Јв‰Ўч&6*xVOtДˆЈ1wdѕiƒš)$ZЃe{j+Э”•ъЈЄIkІА…HчxБŽЪУ}ЋWГœ]кAJTІe+#ЊпЈfтюЙF ЕН'2к Рk,еѕq4F”#хhФ{к@ъЖ№(еQAєH"Ћ*‰&jVЄЈЃўЈДѕMт=EZЧУѕ;A‹ќб*ЂPЧ5Е“S2(жб €Жfѕ?МZ’K!šд"ЊъЄ (вВVh‘ŽКЋЅ=bQЂб#ДB#YЌOPДп5+шmPX­6л‰nтЛЋЧЉМj&C[ЄХ‘’Ђg2З•Д-Ы>^tdгHIˆ™БВx#ŠФuaЂЮ-”H%•iЇвцб<ХВКяlћEѓЂ[!бHЯX4Я eaжсyФyУŸїмЭ{юц;чV V—E=b[ЙгЧ;‰ЭыhtЁЋџыЬ_ОћЕ_gwŽЧ ђЫТЮ\фќЈt…Г#ъэXёB,2џBЄOh–DKо†ъ$8 B#Ђ?xяЁ~шуѕйїхŠш‚x-иЗѓquP†8шЃС`tоˆЎ`Ђ Ќ"шЪ­лcu?#aˆv: G9мр‹ЄУtю“†DпёёГgџуЁ{§Зџш3eDM” Й4P‡ШЂиiле#К‚‰‚§§š:кГ;НДѕхD…j&УЁр™:к /Дмыбt—П№gЃGўхпљЯџ~ЧkЂ ЂКŸC)*jWдcŽ*‰Я_ƒш &ŠRЅЄ'сT’hЛніЉe\1Ц‰Ъ бgъЯ3ўВ_ћѓ§Т?9ѓЙўйŸADi9*Ш^p0Pƒ'уBˆ,™€(ƒвсрЕDQ^0 Gъќ4DП{FK5бЖGЕ„(џЩНѕећ J”ѕ–НЇxђ{џюѓџј№™Яž9ўўяеi #?Ђ!#к™ ЮЇ6Ё{Ь~L”F •фw$3?юЉЌ`Ђ8бк˜ТЦs \свый.чŠ^иФRqЂRЂЂ Дп* Ъ‘тyЦOџпQђdQПDG=чОQ?x@cDSBд_ФО%†&ИЕK‰NђvžЋw7яц“ц­аgоћoПљmOzђ/ёkеQџGнb­а‚r НaЭ{E`%J4ZзŸ‡и“?g^гEJОё#џsє—ОјBVЎЃбHIA]ŒqFбМВпŒЃyГˆe^ўюџ§ЗSљЃ\GПDx0tФ<ќyФyУŸїмЭeЎЃsЋПt‰.ЉWžытГŽЧ4ѕLЃ›ктЧЕ пѕнDcМ–ˆlЂdš >чњэщŠŽовДGКЎ И‹эš tдlOI”ЙМ5*›ао› `MЯЉr-Gњ #=ыњЊп5ŒДFыфМq2;ЂћФ‰Њ•жlМ‡(гQ“ОњbХcђŠ‰ъ`ožOъ˜-ЮчCmйЙŸŸПМЪў*ЂшЩНОˆ<ЈЫ>CЂФJŠˆЭЪ:QrScœЈ“БЈM™фˆЄХDQмПaF‘р$ы3#ъ§DcЕŽТ‰К9{„шx':ˆjЄІааЦDš:„wб-љyыYY ЭBЂaвПЂHEєœѕъњ EъЃDеЮ§NњЊ0Е:Š8VСhw?жЪшO”ЄаŒ'лŒ-8''_ЁЃЙ Šяm ЪQGЦтOиыŠfЎЉцОеGˆ†(Т™ QQF~ j6D'œh>ЉMдЈ*0цfŒѓ•сДŸH9Q№МЈ`BѓљДњрр’#х8KˆвК9” HВ7ё>КРаф[G=Ђ,Ј™ТgŠ(&zМЇ)ˆšhyŒ( ‰кš)zSћEІŠ-L”љ_ъЇёН':UітЅŽ ЋгІъ6#q]П)4ХзіˆЁъ 5€"АDхDЏэD4ЊЃEu=Ењ*ЂЮ-И)вBD™л"9j|w/IмLйэKД№МžŸд'ŠлL[Œ”јC2Ї%zў#%гeѕ­ўBDѓjzO0šзŒш|ч<†?яЙ{ьЩџбшМ6ЖGЈ7IENDЎB`‚d-rats-0.3.3/images/aprs_sec.png000066400000000000000000000106651160617671700165050ustar00rootroot00000000000000‰PNG  IHDRQay‚sRGBЎЮщŠPLTEџџџџџїџїœяяяцццџџœџџЮЮџЮЮЮХХХНННЕЕЕџџџœ1!џRЅЅЅХœkœœœŒŒŒНџџc„„„џR!ЮЮcœџœc1ZZZ„џяоBBBХ!Юc1:1:111!!!1œџЮœ„CžіtRNS@циfbKGDˆH pHYsУУЧoЈdtIMEи /:~6A€—IDATxкэ w›И€uzЎSJЗэЦДьжmж[лэќџПwбcЄ‘4z€уlвƒ&фЯ3šб !`MOš&­;/иЙ"ЙQ€к|§ьќъRХсќ…њKПгpСсржvSыpХсANd|ЉОЏ ЊQ~ˆ48!‚4WќІ(чPsјGЂ"‘5˜B6*™ЂfЮх,|QNžPqЇ•MмЪžњэЋdTС‹TэН*~:ž(J&Y46AОHqNїE7ц‹жцU$jMbpЁ­WвІyЇKщЎišЗ%Њ˜Ђ†Јјјй ‹фФQbњG&•$J€bЮYDП)$Z–Qˆ‹яIшоь$хєˆJ”њЯеH‡ЁTН(’(ЂjAЪр*PУIбT‹…D' ъЏ Ѓ!аЙD=Ч\ШнуЮІщ)R$:С­їюpT)ўўщ>њЩгr*ZFDеЅQHэзш:счє‘–ˆіЛ]ЁРЌ› б№№РŽdt/QЙ“ˆЉ%Клl6Л€ЈЂ‰‚:“(cігD;yТЎ†ЈжyЋїrйЫƒwћ§NЎ{ЦQ 7 Ъ5/ЈW ‰vћHы)QyдFs:7с0X]D4Ќк>"šеыЫˆюї{‹tпХ":ХOЫЈ‡s б(Ї%њЯbЂц‡п вZ/eT,бz­щ4 ŠH3ѕЈЏђYЫФЋvфЄ.вњ$бjЫєЕЮ{b,г^i{/pЋ@4Дѕƒ•QБŒ(ѓ#ƒt‰eR,Mђ FЕїTI”ѓžШНJ§боЌsўЈЕMiоУџ{ј№‡Nѓ‰цyOš(.ŸнУпЃлЄЪ}? ЇZ(Ў}ЎЭ$Œ{HѓўGЦУџC2…ˆЈSњЙDˆкеГЗBїЪ>Ё‰в†­?НZњОдЎА%Z  TFС“Q Ъ5™цЖBЉв‡DЏ)љ<%Эs/7?/  мN>RёQTН0R’sЏЭћ7HKˆ8љ†ёхбМ5dМЦ№_*б5­=wЋжЏD_tёOSЊш |O’gжk/фљНкЖ‰ю”яqJCD”u=_кЮг‰"ЅюІчzО EЂ{`ƒЂб…vLSТ ДHеЮQђдЅ‡šохЖSУДL=w‘|xџžк ‰дняk:(w;)8 Љк9•@'Џ’ш‰J)$VYЂгв…~§r@§2A.…ƒщ~‘"*ъї№zeTœh;0hjЅ—$q9WFD-KKt8)ъы•QJ4ЌѓDљzt)бёdEє4†DЃp§Kмy:вs§€(ѕJЖ>КSRCo‰žбqЦуI +eЛ”^рN"ЃZHSюO2і‘ЃФ_(CєŽжЃšш *гaX‰."Š[†шрˆ Gє5i}D”гzЇєz+CtЎж“ UŽFыбзl™ˆŒВ–iб4AдZЅпˆ(ч=9  щ\Ђ)”#ŠЎгдbzў(WЦўѕˆ‹ЏќQы‹ЪнџMˆЦ­P ЭњЇ!*BУЄлLІ*>jзП†H њЃ'ч&"%­Сйњ‘’б№B\ЛžКї6R2œu˜dМг"њъЂy\›iV4ЏіB\ь)ЭУHоАFœзˆѓкsЗім­BЖj§K%ЪЦк -К”НTЮ оh7ѕyњo;Нт.ЅФ3щN)]ЄфЦэ”фZ~Z№ мy&i)бN%wжDЮОo}иіrY"*h§# акO­цZq;в8n6Ѓ‰=YШxV`(о~RH5н‚7ЬŒrlсџяѓOCѓЇA іњžgЇvЖэєзт ЯкйфBgгПuv:ыЩ^&{ЛЂњЗЩоВŒ-ѕ#Єmq€™"К)•Loд_U‹7кћџFЩDYЭ•€%hзљH§œъpA6‡sЖю7В9}œVыyНЫU"Jˆ лрЁ2zћс#Ѓ—eЂа:ЂрЅk$JDкXМОoœŒy&9­…™DЕˆZVDЁXђDw•DщВNFхŸ>ЋЋAЭ–Э VјQgž|5шлаE№„жћb=:uDŠ(§unMДBFэ%ODTISX:“$Q"ЭЎvІMДОpыoыyvmСжR6ЈЉs„ПtDoЂlЙIчIˆђv™#ЪчфˆВ9yOczYrmЁїHKЇ6єЎЋйCžhЂCkбъz”ЊrVFБ^Є5.+Ѓ\Nіъшйљм9eКБ@-QlD› ˆN9ќњaž­W&$Аѕ,QЋєZ›ГZЯхџтЁEj[_—Уœ“tŽh•\јY›:@‹gќtжƒ" бŸFп‰иzж2YџВH”ЭI­М§і-лXсrЪZtу89­т{DoЅŒо,“ЛѓDє<=“sіНr ЋGЯГˆ:[Ÿђžœ&чe”Ыщ u‰‡ьE9ЧбJgбл2б›Г8пи—8Gни‹ˆђѕ(яс[‡ЈTВ9гŒ;ЅздšŠАЭф7#CkрpLM•ŒоL щsоoю7›ћ{БPFm№ИzЁ­wъ=щкГЕЮT*g$Ђ%Ђ2ъѓМЙСТпя6;Iuз~"D§XЩѕ"%ѓˆ†^ІХЉў•ѓG#Mv9џ5%”QЕŒ”мœ§—ЊкЖ’ч6§ЇоEJ OЭ{ЂQЋЃ$ВТi3ђ‰hВœ’т_ZFџ ‰њ9Y­я'ЄЛ Р=єХhо фРУ_{южДім­Zџwўјqб0юШyZ™І 7гиќ+jGзŽœvў вEœє­Чѓgэ›вQЅЂqыЮ’,E‡ЫвФQЏrёЧQЯv0Ž5DыoD3<›гВУЗл`чбІ"Qц6a…Rœq•%ž Œ„ZА.Њ‚8КшАн Ѓ%ŠЧDыo–д<Ÿh§с!бубGš! Д-–Ј0Dѕкє}=о  ьž/BЭ{‚huTУшЗкРtОЃ1$*.!*y†2ʘшё Uw;š7™Ћh4—ŽQ-ЅцRЭ6šџ”Фe&RgНGb4SЩ)UmpZeтн )ЂхrЂJ>з%šИЇФoC–hЭ“2ЪЇ“!Š2њEийyД”vА \jЂЃ?)зјTDЯfЙж7л)5nчё"С) nЎ!JЂ?#ŒЇб&<›#*QW.%Z7NG“<ХDыW8uj–№ˆžu ЈGyЂ^вZяЩ(jНАъЎЊд<би2U%гђ­>М70q#šвzŽЈvœ[п‹Q‘–QЊђж2•‰N‹ЙD›‹ˆ*бlГ(ќEFыyЂм]Яt=ъ”ЗjД^šЅЖHrБж3–Љ,ЃCС2е5[кwЦкzОЈ"њХt–iQ$O,бХЖ>It№ЊгŒї4“(l‚оHЮ%H‰ˆчМЇхDUzBЂIяЩsGs~‚hуKуэь„ј –б]04Т+2}мууfѓјHŸИ`\:$рКѓЎŽу6”шоџNѓ‰v]%МРѕL?р(1ZšГSЏш.МIšЮЗOzA%P‰єќа !˜ъЮУс8BШˆ†%3*ЂфOˆЪ;аyт˜єАЂЅМ-‘JЈ“UB4š–Ž($!”GGє"Ђ"пgЋ‘ƒJТ'jgБtDеK”ˆ’{йё‹~#Љ@д•ќqiЁв*žњr)^нD„:OFуi9ќчТXA3@­ІdдяЮС—б#ЃШЈќъ]”QсИаDѕлЌrDн?№FЊpDнЈKДѓЂ–%ЪLŠ?Х›7~рnћH‰>ni=ъeЛѓlЇ˜0@i=X&'ЃD=ЕŸG4)Ѓa5ј]ефАj‹ЩJЉ&КлHе‰'Єo€ЕЂ"ŠBš ƒГнynмНжљœ­ŸCTвєnЛіДў[.\Ÿ–б˜ЈкuR™ѓ­KЇѕЛ]ˆetzН•}"zˆ‰r]5уй <ћЄ˜?Ofыэш–шƒ‚š|P\JF#УВ—D7нїНDj‰vнлЗ[ƒT$CTn|€&ъ’“'šшЮ#BкˆŒ­№R†Јf‘Œ~ћЖЫMињˆшwCєЛGTелЗџ3LsZя? PфˆВ_lwлфk}dыke”\I§6ћ›БЉ.j=%ЪOjЦИtлGњ$'ДLеЭћ  ЭљЃMг|w •:ЂTы'ЩFЕŸЖцЦ№ YЫd?ч=žYyЯЦЪ АЊЧСЎT „цъбZЂђSюэЮља<˜УежхDYящm€4ясY ъсc‚B[$jDк”кѕЕDЃ‡!"бKєa6QЖф<ќ бШYАbЎ4џLыі‡ETzЯ2‰Ї&:/(RC”k…ІЕ>n"PAGХžPшќVFM=*яШ.­БL)­Џ”pDYч‡”lЗKЕP\ЗЙhА ЫъhJ”ЄД”NЮгЛFd‰Оиhоо‹цmЗSЅВUЫеЭїŸEœ№LZcјkЯнšжžЛUыЂёШqзф9„gUѓу]НјЬœ‰Œ65йцs 5fvAњ4ќГіНtњЇ.iP$ RWVn&>оюP“mЮqR+;ўLќˆЮњdщqЁК“шНдєAš,дІEnЂ:!НЧ0лэЈч‰'лLM[ыђMC№:)sDЕЄтвыu ‚№JDЉŒF]grчндКS­ХЛeDЉM%Dу пЭя3О Q€ ˆR[ >Qсz#QDЊзлn9—hЊлQ?ЬsЂЄ}.–i}‚ЈHЖЇ‡5HхсњЩŠd™бz ДKGоh f9ŸЈ7ЧlYF!ЗqŠ={їИЮy8јHнс.|еHTЃЖ2Эп=Љ#Љ зГнŽ.˜хЩ6Йб7.Єяјq:‘­ч:`QOы“DEŠ(r•;#ЄIЂ\‘К.4іщN29лІљ~­Шe‡Б!™д,Сx„ЂgQ'Ѓ}šшŽ“бeD;š“QUŠ–ЛЏсyˆfЕ‡ц‰ MЫшпwHsкњ{‰жWmŒЖmй2qнyTѕГZŸ$Ъйzвш>˜~vуzD'TDƒЎ3%Ѓ2*l€Ъ-JєˆыМ?*§PEє>KД}[ж6—h•ж{"jˆ6–hcˆЪйЧБЖCšџг]sЇоЪШ5@+ДўYќQŒВЙбЈы,9%ЋТi_%Ђh—ыђMэu‰ЮQЦMэYšл„ošYDа Q:йцHД*R’ДѕešATŽ5Lh})q<;‘#Jq^3R’%ZЭKЕ™Ф,#:g|ЭуЕоMЖ)jˆЎч5†Пім­iqњ?‚ mџ* IENDЎB`‚d-rats-0.3.3/images/chat-addfilter.png000066400000000000000000000006201160617671700175470ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆtEXtSoftwarewww.inkscape.org›ю<"IDAT8ЅS;NУ@œБурQ! ЩR.фrœQD>PЕ[„8„ƒ‹tи dУкi›}ГЃ™ЗяQўƒV]!‚nЗ›ћОŸFЃМŽG“’млпЭIц’€›­NEЫЏ\ЫЄ*I$Гщdf H2],FЎёВdХ‘ыКFnЅуёиŠуиђ<Я‚%–”дn;ЭН~(№vЖEJ­–#’,ьзtљвы‡ŸžјЭС|6/œ“’ Žурhpl—мрюўжA’фхŸБmMЈœ_œUŠЇ'CРѕЭUžeYsV™VшѕCЙЎ[8“ъц vзБџЃ@šІ љ6™^јˆ$‰qыŒЛ№4ђZx>kœIDAT8Ыu“нK“qЧ?лГ=sлу^DЇ+IЩPч ™[$”fJ’а]]t]Eє7DW]t'нˆ]дRJEˆљLз4ѓ­|™3$]ОДЅ{{К(mcuЎЮяЫљ}8чЫ9QпфБЭР ў-@(Јј!уs30ъq7QлPУе+]ˆЂAШ#ГЙafvŽž‡НAХїђPпф9WhЗѕt? ђ=BпГчL}˜deiГйШЅЎNЮИн8Š Р›З*‚ŠoQP\ъ\zќЈ›Dbй,“Ÿo`umш(Z­Щ ‘NЧq—рvŸdppшЮњZјЎі`„“Œбh"™Jтnyšы@{{xэѕRпPЧd0Ш„œееLTЙjubжfхe>$DeEŽвRšнЇ),rр(qRуЊХbЕ`.‘$щџAаjђаыu//CЋе ›Lфi4‚Гл9€t:MZU5ЇXUUёx––хС^,Ъо^OC#ЂіяЌƒD[ыyŒљf>MЯd4IqЉSy7їƒŠo Гўy§ьVYJчIENDЎB`‚d-rats-0.3.3/images/chat-joinchannel.png000066400000000000000000000012521160617671700201030ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  вн~ќtIMEж 72RуШtEXtCommentCreated with The GIMPяd%nIDAT8Ы­’MHTAЧГыјxО>оƒ3ЂВіЫƒ‹о"‚dнCP."ˆхЅcx УCtмeлЮ]Ф*Кl !ДЋжЅšЫ.Моэyj`œr™тF‘ХЗoNV*•ІXW7RJЧ!жеMРМ;žЛ§@ё%7ŸХ4-ДzЅЅЭ‘(›ЅMЧ[?ЗшŒDяЄв‰АЎы=†a03›AЋзhl<ѕЃжheѕ;Жmяін7†№ќGxўт)љќзR­бЖу`7ЩЭgB|v.[І…зу%lЁЕ5|LзѕЦZЃгMg˜™Э`КЎпIЅWмˆmл”Ы6ЫЫЫMnЃ‡’ш :>ŸС!€8@нNwsНИf4ŸП@цeЦВL‹RiƒPА…PАP(ѕ{ГeZзїRЪOSг“—мПЬЖm„Ѕ@ЁPJБG9,Љtb}umEUЋеЧъкŠJЅыЕWи)хЛЉщЩk‡"Ѕœхфѕзx"DcIENDЎB`‚d-rats-0.3.3/images/chat-query.png000066400000000000000000000015761160617671700167710ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆ pHYs з зB(›xtEXtSoftwarewww.inkscape.org›ю<ћIDAT8u’Oh\eХяЭdfš—ФL5˜Di4 I‘4ХˆRMшТ…ДˆKЃKб 7qужV‚ˆEшЪкlL2m ЦTJБ*–&щДhъ˜П4N'™/ЩЬїНїюuQЄД?И‹ ‡УхžуЉ*;Ь]›Мrхї7WV—ћкZл/їєњЌЛырїCUQUІІ'‡ЧѓЃjLEEDEDЉшx~TЇІ'‡wtwЊЪьмЬрx~TEDkЕЊZk5 CЕжЊˆшx~TgчfяkpvфЬД15›FЉЈuVУ(дэъЖnmoЉ1=;rfњ^>РЪъr_4PЋU‰%&rчQa­%XXОѓ—ЛIў‰уСУcauс/.1ѓч[U‚OЇN§ƒч]T‘7'†ц;ДЕЖ_окк$U—Ц…ы,_§Xреїђ—рЉо'8њ\// є>игЕџXЊ.y5wќд+Л---Г“пGss–t*ЭпЫe>јќ}НC]†Ћѓ%ВMіЕ>u{ќgћКъы’ўщмБ“ўтвтѓз soAРЙѓуЄSiNOШэoЇdBnТXxїЕЇyІЛХ5CЩ8:sЄ=пџШЫya џшqГЙСз)‹|xaУ‡ђЧR…(ŠQ`oS†uSCDI&|r­ќќ[Ёф/..єg›ГDaФŽ92@Ќ>a$8""<іp#чо™ц …ˆрТˆ0О—нMЁББ€ >“Тж>J$JџЁGIј>ыІŠŠ№ЁVГФЂe?ŠЂzї_іЮ9žь|ˆлх A&‰JLзОНЬЮЏa]„HЬžt’лe}IXkqЮb­х—К)•ЪјA&Щ/…>ћO… Фг˜[keЋЊ'’ЉTЪlTж›:rЛ…ъШСЩЗг }ќ-AC=?]Чѓ<‚L‚jЕmll:Uy§цФаМ—??іuБјз‹їЊщЦЖђC!fЉ,иRIпжBљеwvšј/Тфощл& IENDЎB`‚d-rats-0.3.3/images/connect.png000066400000000000000000000007311160617671700163300ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆIDAT8•’ПNAЦПйл“Ђ6%ZйšX˜ј jeЅ…/a †€bb_СGP_‚ЦЂVРсŸ†‘Ј9–;йЕPбƒхЋ&“йo~3Г„.œœ^!6Лѓ^2 уВ'‹I)ЅњKRJ‹)юaLD„щ•ннЫзчŸХУ П::юZИЭ•PО>G<žBlDЃбРЧJ)ЃєTvхљАШ–уnфкСвт ˜Ц@ј)МЭ•:БюƒгВМ šТ†ІiаИЉ$иЖэъh5*§ lЉil›аол#pЧs)%ˆШ}ЦgЋ п(‡nŒСёЙуёD'W.ЋQщAьжък2Тѓ (ѓ"›НIs"RRJњFыЇ/dш\‡Y* 84ˆЂdђьТВЌ}$РїNx[‰єіЮжzСЬГЙй0юM„&&QЋWš˜B­^љ|АП9ћmРˆ*šц7ќИ4СЙŽZН ЦjѕJr7€L&ћŸ <ѕЊНжAoRЬ‚IENDЎB`‚d-rats-0.3.3/images/delete.png000066400000000000000000000015121160617671700161370ustar00rootroot00000000000000‰PNG  IHDRѓџaIDATxкeRiOSA Ц`@ˆБЄ mсЙ„‚ЌqЉkbЂЁTM\bќЂˆь 7p‹ЦП"*n‰њI> (nЈtЁЅ.Џ ”WZmЭёОI ?œфLюЙgЮмЙ РјAаы->юЭt~~а­бЇrsпNЎ_пkЫЮ.\Ў_rBcиP$Њ$Ѕsg!Y:!ѕtРзpтЁƒэъ б‰Еk[ЉMѕŸGЋ}Ј(—Ѕѓ Ž›рлЗ^C<я^#|ц:2:‡P(џШШxЧM DІбSb§ЇNТ[U@]фЯŸr)рм__щВэ№ž4узЦ<љkZZ 7pцфш\Mд_S ЉМ Гїютw,†Dт’Щ„ЮЧу?|OБтбУјКzuєЫЊUzfЯЮЖL—'Єћ4™ŸŸЧНћwаЛ‘H333шБ\СУG'cПй я#&„‚ФЧдд~f]ЗюЭєŽ*xKЗ#2ј ёx ]э0Ÿ0Ё­Н-m—9Пйwз"Џ_СMsqž оГŸ™™A20A€ьА#›‡ЯчУЅІ‹J#Gsk$Iт5YtУ­еТeм…•*ТОЇЇEуnˆ˜Г[ЪdрХх–K‹­”B’ќМ6чvСЕiœ”š|KK{ыЄHтЖ­ <Цм\НзzxcSs#%ЙРyOяU^ Н„S(„uыŒЊTCьSjjяD^^в]QБІГ3!мИuнэ)ю}уUK7юмэУьlЂЉЎвŒЋеЩЦnГŒŽЎ\uVUТU ‡ЇяТЁТс§qAxш‹'uZ8*+0’’"`l3_ЄQЦZЧ2зШЮн;саыр:V џ“Чк­ўЇpжжТІЅЗякE;ЬXзт&*kIyї9+KЖ“РQ^ ;ЅБЉеАlФД…6мiшэяЉgХ‚СЂ EjNI‰ŽoЬOќ*1РОЯШЁ№ёќМ$Хц7/4/, є4œўaК…xDЅRІМyЙўL’}hGXь6IENDЎB`‚d-rats-0.3.3/images/disconnect.png000066400000000000000000000011311160617671700170230ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEж §—АцIDAT8Ы•’MkQ†ŸsчN4U„~‰4)Х"и+ѕDAаКP7ZарV(iЋPЊў)єИh7ї7•тЂБfšЦbО0ј1ЬЄw\„ІЄбОЋsчžѓм{^сˆfg_.њО?ЮШqœЅcЩtzЦcЂЩЅг3‘nгXD„СЋ;N/Ќ.4ŠO‚ќ3Дљ˜Ћ‘йt)Ќ.05ѕэћўЩЩgˆHЧЫQ1=§7_hЩы“"{ЁkадишJe)„УТЬІлŒэXaЭkп`ЯА, Kл˜ШP­ў ‚–‰^Ејw‚РX(Ы!„=?ЄОпE†mŸbŒADZзјнлчT\c;=8]њП@„ЇЌуІ†™мд|Г>5pСеGёŽ"м=§•ФRщŽ“Л1Jryƒzaž\б"cф­мд0вGi›ЁЗPБ8z№|УHssЏ=ЯыhЄ‰•vb)ƒuц,ѕJ‘ќН+OмќїнПН•§Ќ†#lяdщыэЇ\)бз;@ЙR„•y’ЫЈXœzщКџЩwŸШнМˆсе—ь–Š91ЖwВhmSЎ”PJQЎБЕнpbb‚љз0{ПЉяцзИЖЖоб…ѕФ%Пм<чn„oўЎДшЯ2ЗкIENDЎB`‚d-rats-0.3.3/images/download.png000066400000000000000000000010661160617671700165100ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆtEXtSoftwarewww.inkscape.org›ю<ШIDAT8Ѕ‘ПnAЦП]Ÿ-9лrP’"\…h€4‰ˆ†‚C4ф ЂМWІуxžЪ\ eŠЅHQ Yч‚IDўм9>пкїg‡тЮ(1wІ`ЅеJГ3ПoцFDјŸЃMtУ ” ђыТ6НЉlэХ#№‡'xПЕ=Šbѕя Q„=чС`?тснEMЪѓ™KŸ<хщ€$•#Єoœ(™ ˆе89m!JŠ;аtУZ№n(kJЉ+3Gq‚JЅЌЩaшъ†5яxТˆњѓЗ›ГЕ™WюнЉq­_†шЄ с$ГзаЊWСУaїWмqŽ€hYицБѕzгїƒэняС(Œ ‡q&’ЖсњC d„УŸ'IЧ9>бЊАЭГ+бFЯыoлВVеРй 0WŸзыЋЮС‘›ыиewuУЊ2ЦОДn6o/--VN=‰ 'ŒЄЄЮAЗЂa›ћ—Md“ыб Ћ Цкѓ sѓ­VГфЙpœюˆ лlOnс/@бСизFЃ~уќм€шЉАЭЯykЬdћ>x)lѓcnQсНѕьЭѕiџD„п$z s#эIENDЎB`‚d-rats-0.3.3/images/event_info.png000066400000000000000000000010541160617671700170320ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEж 4'Ž ЋMЙIDAT8ЫЕ“пKSqЦ?ЧFsщЇ˜?&!RKЎ •zWi BюBЅдЭТЄ‚‚Й Н№*'ˆBбЂy4W‚bˆ:ќ•ЙЫЃ‡Гo7KЦБХ$|.Ÿяћ<пžї…џ„d$šД]РSУг+рєNg5H‹ЇК=8Хи‹,h:Є„`vi‡—Ѓ ­™&y†_ќў.7юZ;’,MhФiы4зW№Аэ:€/S`4ш(ЕYPSхХъ*mдUкАх›IщмlЈИћ/ŽЕяУ[ЌяХOИнX’OЋћ\Вšь™ѓВб цr‘˜JhzЭЈЪтЪ.сoпщМгРђЦСЉŒ †о~и љZюЋ˜LylџH№ Э…ЫQШЧЅ€Y(Ao?ŒєXЭ””!!ё3ЁbО`"МКЧ;%‚єњsйƒЉчїбIŽд(…еН< н@ zЅœЖkfЁЦц…Ўk"2їLЬ…œbfёЗY9‹‡O™№АДЂ&ОrQŽъ?kiмэымК7†УйC’л3%ј<~хD`Ms9г8]з€ №-Yф<№п”1ˆˆz~IENDЎB`‚d-rats-0.3.3/images/event_ping.png000066400000000000000000000011231160617671700170310ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEз}lѕIDAT8Х‘ПkSQЧ?їої‚$Сг$ЃtБ%­к:й)КФЗ–ЖЦхQAџЭ$…Ж!1 ў*’,у`‡:ˆКИmAJ­‚,ЭЫuH"в!vвГ\юЙїѓ=пsќя€v@n“k?€ЏАЇЗїРўСcsugѓАжZuЌ(„cцЛ7Џ_]ЌVWЄИ­у'fь’J))%R*Tыlо%hд‹ХХ>Ч=[­ЎФ лЖЛжжWћ‡G†фЕЋзЗхццŒ,—Ы§ЖmwЃЃЃО;љŒTЊщќбƒ'“рГчЮЄ].ŽуШh4КЫB%UЛЧл[˜@€цАзт‰XZЪцЌ].—2Є” э5`№Z–u*ђ?\8јЇВRJKŸЯЗЉ”jhнLЦБёDь3 -Ы:љ Йlўї@Н^G)е№zНu#‰|ƒЫKK/УЉд iš&гг—џ~.›gcу'щ[iкpхyЅбгZ‡У5У4ЭеЉѓS™ЙљљЩЪГЪ>Чq$€жZ‹ХдјФи•\6ЯјФXлvЃлп§1y!™qЛнŸDыѓ!`ийjя‹тЉжzoЉTJо+мНМeC5р­тНшДВ–x8 ˜Р‚тлп˜ПOD›?їgњ˜IENDЎB`‚d-rats-0.3.3/images/event_posreport.png000066400000000000000000000010401160617671700201270ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆ pHYs з зB(›xtEXtSoftwarewww.inkscape.org›ю<IDAT8•вЭ‹1Цёo2™Ь4™ЖЖ*ЎьeёO№фUёфIФOѕЎžO‚‚ˆKYиVЗгїŽv&‰‡e }qЅПc^><<‰xјшСѓВ їЧОХЗщЛŽ ЛЗoнфнћмИw“YђбŒо`ЪЯС„|lжє†­,!Kc")hMх&Qt~ЭЖЋ Чїу1Б’„pšЂr­$Ÿ›%њЕ‡Н _;'шГŠzэ7Gљj:&з€ЪUРdОX~Є|R"фцЋЋГЪВтё“WИmХў5K2nt=ћt№ьщ‹сN€RjЄЕЮ•RQY–ўџWжcЭЧFГб5жьEQю Xk_6ЎЕvЏвпоц9ѓ3lІд‚й1IENDЎB`‚d-rats-0.3.3/images/event_session.png000066400000000000000000000013131160617671700175600ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEз№ ˆ№mIDAT8Е’[HгqХ?ПџМЎ(ems‹M|R"|ШLD}ш!Л•^оwэжјНoЬ‘BЮ.B—іП§Вшhn ž˜§BгзЛыВаф2€Ћ–iЄ,гј99|rчємтБ‹—Џa~qVjŽRЎKvЈ—Z›>Љx/tЖ7uАjlK":њfЯцRGKMaЄn–еО№УYМИОdцзяŽџSМ?1]$@€SQkА{…UКЋц§ р(€ђ Рв2рœџј= ”bмT‰пЧž‚љUЫ42ЕSg†Ж­Ќф7ќ0ќЫM ю &g—с‘@тЗргксюў!?%{^й485=з @А*@"шl­Gkc=><аGЁ яќЦ=чFЂБјю*уˆG—вšJє8йKDeЫ4Ъве™Э—pѓ^’пъоDm!кћšod|zз_“ГзЃБx&ЅЯ)!4"ЂЋ ЋжшвїЗэ™љLђюиTч;}]ўPа‹ŽЖљќ~_z)єГ,*Ž"!2 щ˜Я€ЈŠЃ™љЬ%kјфіхlў§я~њsщJтNnьЩ,цsШйe0œrЉ’YHл`юЗLу!ИWј РЫ4Ињyt{‰Ф1€З1s˜ˆвў`цг–iќэЪ§ъК^Ў;O=СIENDЎB`‚d-rats-0.3.3/images/folder.png000066400000000000000000000007371160617671700161600ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEж /йхO)tEXtCommentCreated with The GIMPяd%nCIDAT8Ы­’НJA…П§)]иMЂ`—Z_"bЁжQДRаТ yёcы#И‘`‘2……‚1d;a“ньŽ…Иaд`Ђžj8sяwч0Wл+—šžчхљЄ\.wwtx2Я2=ЯЫ—Ы[шК‘š=ПЯYхbŽ1d„a@ѓщЄDJЩЬє,kыE9ЊбqькщqeСяїоo$МДŸY-.!B†šІ4‡a„ыовш†BЅЈz]Чїq+ОadГY5Т•[Їлэ*…;лиvЫš"IqŒ”™$\КU:ЮPкнќ6ЇяПвjнёW–i4n‡)%эіП‘(_8ŽƒHєћ>П•ўсO˜ eЂ’$žEКB,+3bZzJ=!†Kg:Ž]л?8/LšнqьџЁ7ъ4…]ЕвШIENDЎB`‚d-rats-0.3.3/images/map.png000066400000000000000000000016341160617671700154570ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆtEXtSoftwarewww.inkscape.org›ю<.IDAT8mгЭkeЧёя33ЛГon6›˜TЃM6Ѕ–ЄкjЃедV(­%d‘тE оСЃТў Ђё$іфЩ‚ЂнKЁ„њвиДz(mм„i^Hі%›—Mvfvцyžё"АПѓ—Яэ'Ђ(тр Хщpй0ŒqУ0ЮJk§‡жњO`Ц)—ііт P(N“ ~xћЕXъшс5ЛЗЛ&,{œ‡•—#gЕю=xМъЁœrЪЅ[џŽМїхЗу'7?9}В'ŽHЂuƒ„НN;œ д# фoЁТ?–ГўтŠљЭтЯ_”ў Хщ‰7ŽяЯLМSKь=˜ІЩЦітё>т–OЬЌЫ,аїЬ,ž/Ињг%ПВlL:хвЌšМ’Гу,~ўёѕžtЪG˜yЋс…ЇH%у$тУЯ}OWђОтћ­§nЎ\НX}Ф.Ÿ?эЇMcN˜тЮЃЯЈяŽЂД& –ЙDлыхоТЇЌдЧ e‚ИUум)? -!Ф™ЁчФъцyЊЛЧqУn’ЖЂcHЂ(тхpИя&йD–­V–N˜BI( Ќй0ќІтl2сёЄvŒъіAЈш„Птњ ‡~AJ…kЯ,аrЛ‰tЭ aЦ9 оиš"—ўtr;nВО}Ѕ4vL ,‰”Ѕ4нЉ%œъ(-o”ˆћкЬНxЂ7п?Vo ‹‘g8іТpЪЅпŸњЦ‘ PDˆq!Ф9@GZџ мn8х’{АџЩЈЊd~­#IENDЎB`‚d-rats-0.3.3/images/message.png000066400000000000000000000007301160617671700163220ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEг 4 Ўа‹ЙeIDATxкн“;HB†П+ЅvЌ†ЖРЅЁEˆШ!ЊЅ9jъ1Dб”Н”Lˆ( BЪбhЉЅЁ-‡ЂЅˆ Ђpt• J’їzЏІЇС ХЅжќл9џ8ўMтt:ј­F*ruq&šІ§Jћ‘2K™ахюe;ИІ‰jФPЊЃО˜E5bив—ЇшnoЈ‰ šІ‰$od#0#Z:-…дН™Є™ЄHђFr‰Ј,ЭŽJ.•ЛѓНZхZѓM z[63ŽЭŒCў•ас КQн_  гвмШВнюBЗЛ№nвквDСLёYШкыЊ†‹yB‘cќsуЌчЏN [‘F‡њЊF”ŠАЛуЧ7;†ž~CmД“3K~­V+йlžчЗwN/щqwай?  ќюnЏ1_А(†ф;ЇЅ”TQ•Ї—‡=•J‡єG ќ“'њѓVЯЕkIENDЎB`‚d-rats-0.3.3/images/msg-delete.png000066400000000000000000000014171160617671700167270ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDљCЛ pHYs  šœtIMEж6Ѓr8>œIDAT8Ыu“нK“qЧ?лГ=sлу^DЇ+IЩPч ™[$”fJ’а]]t]Eє7DW]t'нˆ]дRJEˆљLз4ѓ­|™3$]ОДЅ{{К(mcuЎЮяЫљ}8чЫ9QпфБЭР ў-@(Јј!уs30ъq7QлPУе+]ˆЂAШ#ГЙafvŽž‡НAХїђPпф9WhЗѕt? ђ=BпГчL}˜deiГйШЅЎNЮИн8Š Р›З*‚ŠoQP\ъ\zќЈ›Dbй,“Ÿo`umш(Z­Щ ‘NЧq—рvŸdppшЮњZјЎі`„“Œбh"™Jтnyšы@{{xэѕRпPЧd0Ш„œееLTЙjubжfхe>$DeEŽвRšнЇ),rр(qRуЊХbЕ`.‘$щџAаjђаыu//CЋе ›Lфi4‚Гл9€t:MZU5ЇXUUёx––хС^,Ъо^OC#ЂіяЌƒD[ыyŒљf>MЯd4IqЉSy7їƒŠo Гўy§ьVYJчIENDЎB`‚d-rats-0.3.3/images/msg-email.png000066400000000000000000000013231160617671700165500ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEзл|жŠuIDAT8“ЩjQ†ПsЋККЊ+…f :Ю ‚tуF0št jкш(шЪїp%$ЈёдЈILмИpщBœа… тьtW*eЅ‡ъыЂ ‚xрР§соџœѓqЎŒ_б†a,$Ib'5z “†a,џV#ЇђіШЉМэћ~˜ВдgпїУќШЈэћ~hЅњt~ЅЮŒк26~EŸ= "4уЯsCџ Рьм§(з˜{0х‡мЫ>Ц5ўžcri`#AИ)ГвmІЬJїттbADу‡ћВџ4xјђ;J ОŸ ччПTЕf|?Š(R*сэы'|xїŠr­оЮZ]“IDqДџ˜ њє{вкЫ8ььл‹—qxўш6 ŸоА#k‘лГ–}л{XхІІgюEmƒщ™ЉЈeрXnкdѓжmtvvБЮяфнЋЧМ|ёœ”ЁШXJ)JЅвoAZЖЅЈыFЋћчp,ƒmыvЃЕІ^Џ7Gz{з„пО}-˜еšUШf{Cё>ўˆ).UV€{ѓe X•IQŠЊˆ(rоФk˜ЧŽx­Ы a™љ м~МО;ƒЁрЫBL\ЎWD„Щ;7#РUїІ&› „jЂщьАкЙЅЗƒ]›VгЗi5еDгX2ЁXќƒAЉК:LžО/Ўhџй‡"шжўAW‡№7­ё.й€mЇiT)—+иЖнЫЫeвщ4ZkrƒCоѕ‰Ћ ђ&ямŠўŒФЭИzhhи™ž™jысс“Юдєн8Š–ФЭИњјёNЋ'i~ч8IчќЙ џ­’$q~xр%Ђ;ээIENDЎB`‚d-rats-0.3.3/images/msg-markread.png000066400000000000000000000015121160617671700172470ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEз1їлХьIDAT8•“Яk\UЧ?їоwчНЩLl&u,у›Эи€ЕD")jŠ? RКз ‚ЎѓИ-ИдUЋД2RjЧqВ(ЉХ c]д‰KА![ЇhKcm2ЭЬ{яоы"3еn|9јђхœя—#ŽМ{ф“x„џQкгЗŒуuЯšју7о|k$c B„8чpЮєћ`h­щмщŒМџС{IkэhрћдчО@)@GФqDEєz]zН.нюж:ДжќаМ€Г–$IvJ)%C™ уу%jѕ*RJ„$IмGB’ФxžЦї}jѕ*ЪSјA€” „ †Œ=B­^Ekчi’$јп‚4ЕњiТp7…B)$РЖ”Ї(CиЕ‹jэОЫ’Ыэ$вTkЇШчѓ„aˆЇŽmМI‰IшuЗpЮАњi>ЉGkнї$цЙgŸчЪе_ˆz[(%Р§[Чќ™y67;LMM1šЫq№хƒїФ–JЅИq=ЫЙ/ПцСАР‹/МєЯ п.P,юІ\.ГММLѕ@ˆ>QбjЕ(—Ым7МƒХХ…э 2Cй-пїг3334 &&іАККFЋеТ€RŠЩЩIJ–h4 ИйЁсЎчщlќЋ гs'–ш%C(&’9|јU2™ N‡JхS.yO`№˜[[bпУ!JЏФ^;xЌ=ž&ћЮ+ћак“ЦКnѕЬ[ЉTвжZ Ѕt“Яl”Її*)E&Š™oЖхХдоkЬkFЦКфт•[ЇWnœX\ЙўMbЌ™=кŒЮ[ŠЌЕvщђz§ЋŸў8yЎѕћyc=кŒ=ќе‰ь…Е?Ÿ\пŒŒŸRУ{ Утžœ77"їнъњS7oї|/ћјC;D?FwіdуъщвшXRt#CѕћvŒрь]Сќgч;АП”Ы8Ÿї9žŽЬk?_Л§сЅіЦ!Y›NИЙTdпОћКџСљЦgт1sIENDЎB`‚d-rats-0.3.3/images/msg-markunread.png000066400000000000000000000013311160617671700176110ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs з зB(›xtIMEз;PрђfIDAT8Ы•’ЭoLaХЯНwм;ЎI;гNSF;ДE?ƒФwТТ–Hь$,jaу?и b%Ф’ˆ щa+ˆˆ:Ѓ"Хh$>2гVЭз§x‹)Z‰Г|ђœsžїœWXе…б,ша щPД\žјЉтєЯ=Yš<2]ККQzuРЫ=ц@nњЉт g чQUЭ‰ш7аЭвЋ]щс:"•‚ьѓђƒЎъТpТOЏЪф6а#RсЪЯYлр3Еээ :ЇГХє/Ž—ћг}Ky ƒ{o Œўqє:(OrKЮ]8wаЎ‡Qиžя§HnЭgF2Є‡CDœх2ІRТЬЖЇŽ‰УkуЧOДg;ГС}д<Ї:=Aа<Э ї "чЧХжХЕф6Ы“ё\—;woc[>`у­;Juњ,jf—tŸŸСЭQ­zŽeY+}ŸОО~<šФВRˆЄZ[ЉeŸ Єљє%‹ "ЌЭхШvnхљЫиVwkIЈжЉ„JA0ёзпВšїЅмт?PА›5ЙffЖRœzЬар)*YЌk ™›ъРэ=‰хьЇRyнPUЂ8ЂйЈSotГОї0/^оb• з]Р)зioѓ‰:7’Hdh†Лq“‡€ѓ‹(ЕъwfчgйГs/ЕZЯыAф –М*иЖGЦпР›З.ЩфЉХxZF‰cУшаfО-Ьcт$ВƒXЗЗЊ‹ ТаІо•ІQ5-'с”у8ъшY›' ’^ђWPњWіJ4YŸяЇVЏс$œВcŒŒ_К|ёzGэќл™3Шјz< "№ IENDЎB`‚d-rats-0.3.3/images/msg-new.png000066400000000000000000000014271160617671700162570ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs з зB(›xtIMEз%ЄQ  ЄIDAT8Ы•‘_keЦч}ggvнЛ!f›ЖfЩVM4I›І5EБН№єОёЊzу'-JЭ^ћ5TМђzaџP0˜ЕY“д˜uv2ѓЮЬћzб$ R8p8№ќ8Я9Т#дНй]№•ў:ЌgЭЉЦŸЬД~vNЌпђ=“‹ њZћTW&Ов^у§oѓЛ7ЛW&'&>~эк•№Й%Kgњ[šе;+~щиrј$И9D/‹Њњ"њW94~ўХg˜ЂИюœ%au“гс›Gs~€СЦ$ К'^zУшvЛ“І(ЎП|љqЮ(“OўVв˜ы#8pЭЙ>ˆ~lkp—дїoЌ„кКsNв§„4(Ьръ‘SшE%ЅН§ъез[‡Sпїc’§‚ |ЈїШQRœƒЈwŠZИJ;Ѓ|пЇ^ЏU”іhŸЅ9yŽJаЂЇЦЛˆ€hМJ‹дTQЦ^|ў2?l|O–eмп|@; 6gpЂБх_DН6QЏˆFаˆџ4їЗCМУеЌ-yf~‘о:Ч['С=†r5Мк‡DНYъ_Ѕ(ђ‡§є;Јк?§ўхf)0?ЗРн{ЗШѓЁРїNQбQ‹bк3m*k т SvпeYrŒЩШsУъ…‹GГ@Юэ;пD'АЪRƒjRZ…JFILk“е ЩsУоо.ƒСЦ8ЮЏ\bkgШ())-ˆ<Діг}’dФђвy’dD<Š.яˆG1бпVЮЎ’›œ,Kоz!3†ЇNЯГЕгЇ, ЌГXыpЮb­Х9ЧƒнmŸ]цоњн1@)•хyЬЮt˜nфПtvсi–ЂDЅВЖіо[ІШзјŸЃЕїі?нz3–‡Ё›ъIENDЎB`‚d-rats-0.3.3/images/msg-reply.png000066400000000000000000000007221160617671700166160ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEз8їЇM/tIDAT8Э’K+„a†Џї}?Le&‰%3Г йM9ќ ;elШвЁ,ьdaaСЦ@БeЇBŠнg1X‡9Ћ1ЦaпМ6rјf„їњОzЎчщПŽј- ‘ЗFЃУHьфTscи fѕhжjІS^P™эшЬsћЖЏЗ–”LЛЛЧП4Ир‰*ЇOф—6‡h5ьАЇo9M ЛШzгуЊЈ*&ЗЈ$ђˆ0>СНSˆрW!ŒLLLš’–ЋмЭsз)„аЦG˜р‰Р,*Цл>ФkщУ“J‡ЯВ“+$•(С•0§шŠŽ1FŒјb?$­/їGe х(o3—›ѓOзЧћУТn`fQю&d™/еРŠseЎ>нsђЉ)78ZFzй@'bvKJNаLd<3ьуН`ЖQoњбЯл3:Ж1ЂM?кNЇЫ›cЭыіF[АВ\?a4Џќн'ўПМ]˜2ш\ЉIENDЎB`‚d-rats-0.3.3/images/msg-send-via.png000066400000000000000000000012041160617671700171650ustar00rootroot00000000000000‰PNG  IHDRѓџasRGBЎЮщbKGDџџџ НЇ“ pHYs  šœtIMEк&6Д†zIDAT8Ы“ЭkQХяЭФd‰ЈбЦв6Š”WЉ"Tк‚К+Ў””nЌЛюќ.ЌŠ‚ўI„"*ˆКmkЊ‚ЕБнˆнїЫвДiвdfЎ‹кш„*еoѕо=чžУyˆ`LO™x8)7аќ#ЬёЇ\>tЌЧякЯ›пЦJ“РLэЃlp5m)†Пг•"чLыp—ЯкwТuˆџIЉЕ[а.C#Д{l›ИсOФїФ:їњLњ^=РП]ќИ!‰Љ‚оІЖзQЛ'N9’-dгŽсQ1,Js§Дv Ѓ542АfЧ4MŠт,чІnЇЌ†фmРЈбw)/>І4‘U‰YЉTF„Ѕ…rя№CЄT§њ lbq№Ё8{пжгU  ”УЮшf§„ъ_ŽUmЏ8ѕ Ыkї5L+ЩhFсj:L@Еcџyђг9wьYЇq<ўзЭ(ТэŒџnK1l3•B.т–?PŸИ ’wk2p)чЉ,ПЌ’ˆІoН*ћˆыuMЧЏF—Ѕљ;8Хœ'ƒ*”pгЛъњUB-НЌ˜љяєkyжqЌkОF€ дl№ЅCя-НЌ(€ЯƒlY(qХає8ѕШ/кА8кѕЦ3\[чП"›FDDВiЄЖЦРц~п)o?"•п‚Н•вЁIENDЎB`‚d-rats-0.3.3/images/msg-send.png000066400000000000000000000010411160617671700164070ustar00rootroot00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“tIMEз\‘Ш[УIDAT8c`hРc„ЎZХ,ѕQSфЯпя:Ђ/Lї740ў#ЦƒћБd‰І.УбѓЏ oY™с•MЮЊЇ?~ў’bd`r8=;ъ 6˜`ŒgЏпЦѓАўќљгњн‡ЏZ ?~ў’šPшЪ№Ÿспгдeіx xїі‹/'‹™‰ІЄ$3+kОJю6v†пџІ•Иу4n#У?v&FuIf}U17с_яПВВ@ЄoПќЦ0ЙШ Ћ!№@ДШXіЂ кJќуЗ? т |\Ќ ŒŒ ї_~eИњј#ƒ‚ ƒŠ8Cnп.ф0ЛрџІiЯ=ќ.-ЬХpущ'†эчž3l>§ŒсЪЃ џџ30\|№сЧпџ MЉv џў0N›ЩХРРРР 3€г*щФЇ—ЏC?|ўСkЈ*ЪЊЇ Ш !УЧРЦТФ№њгOA1^6†ВЉћ˜ЮЬNМƒтэаUl_\'Gm7Œ$+!!љ ,w Ie9Ž*,‹ (Ф*О(ќB5[1В ЅZВгIah!G—ЊexzВьJ0ˆeП6РТ@V|UЋё4КЖDmВ…%$Э›ыp \Gx }@+| =+ 1“- Ea5l)+!!љ ,y )œфЈž'AœKЉ’Џрк,ЂЖ§“‰E\(lƒœЉ&;5 р5D‰Ф€0шГ3‚aЌ0-‹Е-бЁР”ŽУƒpH4V % i p[R"| Œ‘# ™ 6iZwcw*!!љ ,y )œфЈž,K”*љЖФ‹Ј0Ÿ aš;з‹аАY8b`4щnЄ ТЈBb‚bЛxО,БСдО‘ БЫОЭфб( ШНА  % >  2*Ši* /:™+$v*!!љ ,u )œфЈžl[Њ$с ВJq[ЃТq 3™`Q[ј5ј:Š•№IX!0РrAD8 CvЋЩмHPfiОфiQ”ƒAP@pC %D PQ46 Š iciNj0w „)#!!љ ,y )œфЈ. qОЈ ,G ЎJr(ЏJ8 ‡C№ф*Ї†BД,™Ž…ъ&< …ŒДлhБ W~-М‘`б, ‡–ѕЄ,ь>; 8RN<, …<1T] ˜c‘—' qk$ @)#!;d-rats-0.3.3/images/upload.png000066400000000000000000000010421160617671700161570ustar00rootroot00000000000000‰PNG  IHDRѓџasBIT|dˆtEXtSoftwarewww.inkscape.org›ю<ДIDAT8‘ЯkQЧ?ГЛi›ЄЖл4фЉkЃhдЂ ц&žКgЯўНyьћМx=є"ˆ"zQтEё*‚‚д"кцчОёTŒьjП№цЭ№™/3Ј*iЏООYќW]UёH‘‰ь№бDіVZ 0‘5ˆ<ЙИZ›ї|oЫDіЦ‘&В%Dž_НtКžЉвКжШ‹'Md›џ˜ШцDфщъйхrЅrв{лйE‚Ў_ ѓˆ<3‘ S&ВОˆl-/ыѕjцгзŸФЮёng—l.'Э f‘—&ВЕd"їK‹Х›ч&ћљл#Ї   яwі(ѓ^#\Yœ@ЪSй{…ќБ;—›чцмŒH`Ё0‹яy,UJОЉ*#ђТDv@ъы›w€A3/­жšїeЏЫAЗЯў~АzœvћУАля{ЊјР+рv<Сию`џpNЇ–ЧJo0pЊЌпџ( ‚ЮіЦрЏK;dу8Œ:няloєRЯxЈQь&Љ; F; R;];f; n; |;‡;Ё;UВ;< < <$<@< I<V<[<.b<‘<š<Ÿ<Ў<П<б<э< ў<==.)=X=p= u=‚=!ˆ=Њ=Б=Р= а= ё=џ= > >'>@> I>T>\>k>r>„> ”>,ž>Ы>Jх>&0?,W?,„?(Б?&к?в@д@й@э@AA A(A;A PAZAK`AЌAДAЦAоA.љA(B.@BoBuB}BB—BЈB4АBхBљB CC5CJC hCvC …Cm’CiD%jD$DЕDeЫDf1E˜EEЃEЈE*АE лEщEюEєE ћEFFFF&F-F4F;F DF RF\FsF{F€FˆFkFљGH'H-FHtH:}HИHЫHмHуH ъHјH I"I?IPIcIŸkI JJIJeJyJ3J7СJ8љJ2KF:K­K/LJL _L@kLЌLЕL)ЪLєL§LMM'M=MWM kM ŒM—M ­M ЗMУMвM тMNN$N;NQNheN1ЮNPO QOrOO›O ЃO­O ДOДТOwP‹P ЂPЌP ЧPгPтP§PQ*Q*3Q^QmQ~QQ“QœQOВQ9R^K^_^ h^u^Š^^=•^г^й^є^ _:"_@]_ ž_Ј_Л_Ю_,ч_ ` ` )`5`#:`^`q`‹`”`˜`Ў`У`>о`a#a4aDaJa.\a*‹aЖaЭaшaёaљabb $b 2b?bFb$Obtb yb…bœb­bЦbзbцbљb c c c@c Vc`c'oc—c ЈcЕcНcгc цc№cd%d.d =d HdVd_dodd ЂdЌd Лd Цd1аdee,e@eVeue"e#Вeжeъeѓe f ff %f 3f#Afefefчf юf ќfg%g5gFgOg=fg ЄgВgЙgЩgмg№gh %h1hIh1ah&“hКhТhвhиh јhii',iTifiyi ‰i—i Жi РiЪiвiфiьi §i j(jBjJajЌj4Ъj1џj,1k(^kх‡kmlrl"ŒlЏlЬlгlшlmm,mX2m ‹m*•m&Рmчm@n&Gn@nn Џn ЙnУn кnфn њnHo'PoxoˆooДoЦoфo!§o pj+pr–p7 q Aqbqpyqrъq]rfrlrqr'vržrЎrВr КrФr зrуrьrєrќrs s s s+s3sJsYs^sgsiœ/eљКzЦ“LˆmŽUЌOxо˜)!gј!3„’‚‹зkf6: сh6Urž2ђп^;7~8щЫO SŸёрDЭ_#$y*GvsJV>GўE14…,Й?ŸCN9B[M†3jŽqA‚†™` ТяМ&д+БЧƒ"{ьP}ЅЊй&…qЁ7„(Е=˜k]ЄєШќx.ЩЯr$~ф;\]< HšR=m5'CwQгІУ}њŠpl)M‡ѕ­F c—tА–oŒ -€ƒjRšѓ‘Zчdџl?Xp%‡X WIeЬ +{FЉK|цn' ZW0С|—ыуП‰.‰ Šі^1’N@ЇВ™œХДэ§zЛHA”Фiжb№@-Tn/*•g0Ѓ"‹_9ЏЮъVYSasїYž”Жc\I•atН“ 8ОTЗишГквu o>ˆJ:Ђ‘ЪKЋ›w(Lћ#P–€2db5тQ`[hE%uЈv И<,Ўю›хблBнае4fyDмŒР on port when a new chat arrives when new file activity occurs when new message activity occurs%s Color9600 for mobile radios, 38400 for handheldsMy StatusStationsAccessActionAddAdd FilterAdd MarkerAdd Quick MessageAdd a portAdd filterAddressAfter this many hours, a map tile will be re-fetched, regardless of if it is already stored locally. 0 means never re-fetch. 720 hours is 30 days.AirportAllAll File Transfers Form Transfers Position Reports PingsAllow POP3 GatewayAllow WL2K GatewayAllow remote stations to pull filesAllow remote stations to pull formsAlternate location to store cached map imagesAltitudeAmount of fake data to send during a warmup cycleAmount of time to wait between transmissions in seconds (a positive number is a fixed delay, a negative value means 'randomly choose between 0 and X')An error has occurredAn error occurred: AppearanceAre you sure you want to clear your map cache?AttachmentsAttempts per sizeAutomatically forward messagesBaud RateBlock sizeBothBroadcast LocationBroadcast Text FileBroadcast this locationBroken ColorBuoys (comma separated)CallsignCallsign ColorCancelCenterCenter hereCenter on thisChannel names must be a single-word Chat activityChat fontCheck spellingChecking mail forChoose a formChoose a script to execute. The output will be used when the QST is sentChoose a station whose position will be sentChoose a text file. The contents will be used when the QST is sent.Choose the port where chat Clear AllClear CacheCommand:CommentCompleteConfigConfigure data paths below. This may include any number of serial-attached radios and network-attached proxies.Confirm addressConfirm exitConnectConnected to InternetContaining text:Control: ACKControl: END session %sControl: NEW sessionControl: UNKNOWNCreateCreate a new message for sendingCreate filterD-RATS StartedDPRS messageDateDefaultDefault GPS commentDefault is 25. Set to the value given by your ISPDefault state for private flag on new formsDeleteDelete FilterDelete the currently selected messageDescriptionDestination CallsignDirectionDisconnectDisplay information about packets seen that are destined for other stationsDistanceDownloadDraftsEcho ofEcho request ofEditEdit Map SourceEdit Map SourcesEdit SourcesEmail AccessEmail AccountsEmail address to set on outgoing form email messagesEnding sizeEnter a WeatherUnderground station ID:Enter a filter search string:Enter a message:Enter an address, postal code, or intersectionEnter channel name:Enter destinationEnter filter textEnter remote callsignEnter station:Enter text for the new quick message:Enter the URL of a CAP feed:Enter the URL of an RSS feed:Enter your GPS message:Error starting socket session: %sEvent LogExecExportExport MessageExternal GPSFailed to connect toFailed to receive file (incomplete)Failed to send file (incomplete)FileFile Transfer PathFile TransfersFile activityFile is too large to send (>8KB)File transfer of %s started with %sFilenameFilesFor a network connection, use something like 'net:host:9000'For each position report recieved, change the callsign to 'callsign.datestamp'. NOTE: This will generate a LOT of map pointers, use with caution!Force transmission delayFormForm TransfersForwardForward viaFreshen map afterGPS LockedGPS Not LockedGPS-A SymbolGPSd LockedGPSd Not LockedGPSd Socket closedGenerated atGet versionGradually increasing packet sizesGroupHeadingHostname of outgoing SMTP server. If this is specified, this station will be a gateway for email forms. If left blank, this feature is disabledIf a station was last heard more than this many seconds ago, do not assume you have a clear path (ping it first)If enabled, attempt to negotiate TLS/SSL with SMTP serverIf enabled, take current position from the external GPSIf prefixed by a ! character, interpret as a path to a scriptIf prefixed by a > character, interpret as a path to a text fileIf the TNC is a multi-port unit, this specifies which port will be used (starting with 0 as the first one)Ignore ColorIgnore RegExImport MessageInclude original in replyInclude the text of the original message when replying (not recommended as it wastes bandwidth)IncomingIncoming AccountsIncoming ColorIncoming FilesIncoming MessagesIncrement sizeInvalid GPS dataInvalid character in callsignInvalid port stringInvalid valueInvalid value forJoin ChannelLanguageLargeLatitudeLength of time between transmissions that must pass before we send a warmup block to open the power-save circuits on handheldsLoad log tailLoadedLoadingLocalLocal PortLocate an addressLocations foundLog chat trafficLongitudeLookupLookup by addressMEMainManually direct a message to another stationMapMap Storage PathMark ReadMark UnreadMark the currently selected message as readMark the currently selected message as unreadMessageMessage SentMessage TemplatesMessage activityMessage transfer of %s started with %sMessagesMetricMiscMoreMultiple fixed-size packetsMy Winlink SSIDNegotiation CompleteNetworkNewNew Chat MessagesNew marker hereNo start block received!No support for resizing images. Send unaltered?NoneNotice ColorNotice RegExNowNumber of packetsOn UNIX, use something like '/dev/ttyUSB0'On Windows, use something like 'COM12'Online UnattendedOpen Private ChatOptionsOutboxOutgoingOutgoing ColorOutgoing EmailOverlayPacket sizePacketsPasswordPassword for SMTP authenticationPathsPersonalPing All StationsPing replyPipeline blocksPoll IntervalPort and ModePosition ReportsPreferencesPreviewPrintablePrintable (visible area)QST Size LimitQualityQuery UserQueue flush intervalQuick MessagesRadioRaw TextReally delete %s?Really delete?ReceivedReceived %i messagesReceived EventsReceivingReceiving fileRecipientRefreshRemoteRemote PortRemote file transfersRemote station:RemoveRemove FilterRenameReplyReply to the currently selected messageRequest PositionRequest all positionsRequest mail checkRequested file %sRequested file listRequested message %sRequested message listRequires a D-RATS restartResize toRestartResuming atRetrievingRun TestRunningSMTP PasswordSave ImageSave Image (visible area)Scrollback LinesSeconds between each attempt to process forwarded messages. Do not set this too low!SendSend D*QuerySend fileSend messages in the OutboxSend viaSend/ReceiveSentSerialSerial port for an NMEA-compliant external GPSSettingsShowShow debug logShow event type:Show station listShow status updates in chatShow time in UTCSide PaneSign-off MessageSign-on MessageSince this is your first time running D-RATS, Sites (comma separated)SizeSize (bytes)SmallSocket session %s started with %sSoundsSource AddressSource CallsignSpecify a .WAV file to be playedStarting sizeStatic positionStation TTLStation mapStation must be a plain StationsStatisticsSubjectTCP ForwardingTable:Test ConnectivityTest ParametersTest TypeText string to return in response to a ping.The NMEA standard is 4800The baud rate used to communicate with the TNC (not the rate over the air)The symbol character for GPS-A beaconsThe symbol table character for GPS-A beaconsThere are no parameters for dongle operationThis is the message other stations will This is the state other stations will This utility will allow you to send D*Query commands to the local gateway (if supported). Type a command here (such as wx) and hit Send. If successful, the result will appear in the chat window.TimeTimestamp PositionsTimestamp chat messagesToolbar buttonsTotalTrack centerTransfer CompletedTransfer InterruptedTransfersTrashTreat incoming raw text (and garbage) as chat data and display it on-screenUNKNOWNUnable to connectUnable to open GPS portUnable to open file %s: %sUnable to open: message in use by another taskUnable to parse addressUnable to send: message in use by another taskUnitsUnknownUnknown image typeUploadUse External GPSUse SSLUsername for SMTP authentication. Disabled if blankVersion and OS InfoWL2K ConnectionWL2K Network ServerWaiting for first blockWaiting for responseWaiting for transfer to startWarmup LengthWarmup timeoutWeather (WU)When enabled, D-RATS will encapsulate its packets in AX.25 UI frames which can be passed through a digipeaterWhen enabled, form time fields will default to current time in UTC. When disabled, default to local timeYou must enter a callsign to continueYou must enter a station callsign. Your current altitudeYour current latitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersYour current longitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersawaybyteschatdegreesdoes not exist. Do you want to create it?forwarding tofromhoursis nowlast seen atof sizeon portpage 1page 2page 3page 4page 5receivedreceived fromreportingrequested a mail checkretriessayssecondssentProject-Id-Version: D-RATS Report-Msgid-Bugs-To: POT-Creation-Date: 2011-01-01 14:08-0800 PO-Revision-Date: Last-Translator: Christof Bodner Language-Team: Christof Bodner Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Poedit-Language: German X-Poedit-Country: AUSTRIA auf Portwenn ein neuer Chat ankommtwenn eine neue DateiaktivitУЄt auftrittwenn eine neue NachrichtenaktivitУЄt auftritt%s Farbe9600 fУƒТМr MobilgerУƒТЄte, 38400 fУƒТМr HandfunkgerУƒТЄteMein StatusStationenZugangAktionHinzufУƒТМgenFilter hinzufУМgenMarkierung hinzufУМgenFУМge Schnellnachricht hinzuPort hinzufУМgenFilter hinzufУМgenAdresseAnzahl der Stunden, nach denen ein Kartenabschnitt neu geholt wird, auch wenn es schon lokal gespeichert ist. 0 bedeutet niemals, 720 Stunden bedeuten 30 Tage.FlughafenAllesAlles DateiУМbertragungen FormularУМbertragungen Postionsmeldungen Pings Erlaube POP3 ZugangErlaube WL2K ZugangElaube entfernten Stationen das Abrufen von DateienErlaube entfernten Stationen das Abrufen von FormularenAlternativer Ort zum Zwischenspeichern von KartenbildernHУƒТЖheUmfang der erfundenen Daten zum Senden wУƒТЄhrend der PrУƒТЄambelphaseWartezeit zwischen Уœbertragungen in Sekunden (eine positive Zahl ist eine fix eingestellte Wartezeit, eine negative bedeutet 'wУЄhle eine zufУЄllige Zahl zwischen 0 und X')Ein Fehler ist aufgetretenEin Fehler trat auf:ErscheinungWillst Du den Zwischenspeicher fУМr die Karte wirklich lУЖschen?AnhУЄngeVersuche pro GrУЖУŸeAutomatische Weiterleiten von NachrichtenBaudrateBlockgrУƒТЖУƒТŸeBeideStandort aussendenTextdatei ausstrahlenSende diesen Standort ausUnterbrochene FarbeBojen (durch Beistrich getrennt)RufzeichenFarbe des RufzeichensAbbrechenMittelpunktZentriere hierHier zentrierenKanalname darf nur ein Wort seinChataktivitУƒТЄtFarbe des ChatsRechtschreibprУƒТМfungEmails checken fУƒТМrWУЄhle ein FormularWУЄhle ein Skript aus, das ausgefУМhrt wird und deren Ausgabe gesendet wird, wenn ein QST gesendet wird.WУЄhle eine Station, deren Position gesendet wirdWУЄhle eine Textdatei aus. Der Inhalt wird gesendet, wenn ein QST gesendet wird.WУЄhle den Port, an dem der ChatAlles lУЖschenZwischenspeicher lУЖschenBefehl:KommentarFertigKonfigurationKonfiguriere Datenpfad unten. Das kann eine Anzahl von FunkgerУƒТЄten, die an die serielle Schnittstelle angeschlossen sind, und УƒТМber Netzwerk angeschlossene Proxies beinhalten.Adresse bestУЄtigenAusstieg bestУƒТЄtigenVerbindenMit dem Internet verbundenTextinhalt:Kontrolle: ACKKontrolle: ENDE Sitzung %sKontrolle: NEUE SitzungKontrolle: UNBEKANNTErzeugenErstellen einer neuen Nachricht zum SendenErzeuge FilterD-RATS gestartetDPRS NachtichtDatumStandardGPS StandardkommentarVorgabe ist 25. Setze es auf den Wert, der vom Internetprovider vorgegeben wirdVoreinstellung der Privat-Markierung bei neuen FormularenLУЖschenFilter lУЖschenGegenwУЄrtig ausgewУЄhlte Nachricht lУЖschenBeschreibungRufzeichen des EmpfУЄngersRichtungTrennenZeige Informationen УМber Pakete an, die fУМr andere Stationen bestimmt sindEntfernungLadenEntwУМrfeEcho vonEchoanfrage vonEditierenBearbeite KartenquelleBearbeite KartenquellenBearbeite QuellenEmailzugangEmailkontenEmailadresse fУМr ausgehende NachrichtenGrУЖУŸe am EndeGib eine WeatherUnderground Stations-ID ein:Gib eine Filtersuchzeichenkette ein:Gibt eine Nachticht ein:Adresse, Postleitzahl oder Kreuzung angebenGib Kanalname ein:Gib Bestimmungsort einGib einen Filtertext einGib entferntes Rufzeichen einGib Station ein:Gib den Text fУМr die Schnellnachricht ein:Gib die URL eine CAP-Feeds ein:Gib die URL eines RSS-Feeds ein:Gib Deine GPS Nachricht ein:Fehler beim Starten der Socket-Session: %sEreignis-LogAusfУМhrenExportierenExportiere NachrichtExternes GPSFehler beim Verbinden mitFehler beim Empfangen der Datei (unvollstУЄndig)Fehler beim Senden der Datei (unvollstУЄndig)DateiPfad fУƒТМr DateiУƒТМbertragungenDateiУМbertragungDateiaktivitУƒТЄtDatei ist zu groУŸ zum Senden (>8kB)DateiУМbertragung von %s begann mit %sDateinameDateienFУƒТМr eine Netzwerkverbindung, benutze soetwas wie 'net:host:9000'У„ndere das Rufzeichen auf 'Rufzeichen.Zeitstempel' fУМr jeden empfangenen Positionsreport. ACHTUNG: Das erzeugt eine MENGE EintrУЄge, vorsichtig benutzen!Erzwinge Aufschub der УƒТœbertragungEingabemaskeFormularУМbertragungWeiterleitenWeiterleiten УМber...Karte aktualisieren nachGPS gerastetGPS nicht gerastetGPS-A SymbolGPSd gerastetGPSd nicht gerastetGPSd Socket geschlossenErzeugt umHole VersionAllmУЄhlich zunehmende PaketgrУЖУŸeGruppeRichtungHostname des ausgehenden SMTP Servers. Falls es spezifiziert ist, wird diese Station als Gateway fУМr Email-Formulare fungieren. Falls es leer bleibt, ist diese Funktion deaktivertWenn eine Station das letzte Mal vor x Sekunden gehУЖrt wurde, dann nimm keinen freien Уœbertragungsweg an (Ping zuerst)Wenn aktiviert, versuche TLS/SSL an SMTP-ServerWenn angewУƒТЄhlt, benutze aktuelle Position des externen GPSAls Pfad zu einem Skript zu interpretieren, wenn es mit einem '!' beginntAls Pfad zu einem Textfile zu interpretieren, wenn es mit einem '>' beginntFalls das TNC ein Multi-Port-GerУЄt ist, wird dieser Port benutzt (beginnend mit 0)Farbe von nicht zur Kenntnis nehmenRegEx ignorierenImportiere NachrichtOriginal als Anhang in der AntwortEinfУƒТМgen des originalen Inhalts beim Beantworten (nicht empfohlen, da es Bandbreite verschwendet)EingangAnkommende KontenEingehende FarbeAnkommende FilesAnkommende NachrichtenSchrittweiteUngУМltige GPS DatenUngУМltiges Zeichen im RufzeichenUngУМltige Port ZeichenketteUngУМltiger WertUngУƒТМltiger Wert fУƒТМrBetritt KanalSpracheGrossBreiteZeit zwischen Уœbertragungen die vergehen muss, bevor wir einen PrУЄambelblock zum Aufwecken aus dem Schlafmodus von Handhelds sendenEnde des Logs ladenGeladenLadendLokalLokaler PortFinde eine AdresseOrt gefundenChat Verkehr loggenLУƒТЄngeNachschlagenAdresse nachschlagenMICHHauptManuelles Weiterleiten einer Nachricht an eine andere StationKartePfad fУƒТМr KartenspeicherAls gelesen markierenAls ungelesen markierenMarkiere gegenwУЄrtig ausgewУЄhlte Nachrichten als gelesenMarkiere gegenwУЄrtig ausgewУЄhlte Nachrichten als nicht gelesenNachrichtNachricht gesendetNachrichtenvorlageNachrichtenanktivitУƒТЄtNachrichtenУМbertragung von %s begann mit %sNachrichtenMetrischVermischtesMehrMehrfache Pakete mit fester GrУЖУŸeMeine Winlink SSIDVerhandlung abgeschlossenNetzwerkNeuNeue Chat NachrichtenNeue Markierung hierKein Startblock empfangen!Keine UnterstУМtzung zum У„ndern der GrУЖУŸe. Original senden?KeineNachrichtenfarbeRegEx der NotizJetztAnzahl der PaketeUnter UNIX, benutze soetwas wie '/dev/ttyUSB0'Unter Windows, benutze soetwas wie 'COM12'Online UnbeaufsichtigtУ–ffne einen privaten ChatOptionenAusgangAusgangAusgehende FarbeEmails im AusgangУœberlagerungPaketgrУЖУŸePaketePasswortPasswort fУМr SMTP AuthentifizierungPfadPersУЖnlichPing an alle StationenAntwort auf PingBlУƒТЖcke in der LeitungAbfrageintervallPort und ModusPositionsmeldungenVoreinstellungenVorschauDruckfУЄhigDruckfУЄhig (sichtbarer Bereich)QST GrУƒТЖУƒТŸenlimitQualitУЄtFrage BenutzerIntervall des Leerens der WarteschlangeSchnellnachrichtFunkgerУƒТЄtRohtext%s wirklich lУЖschen?Wirklich lУЖschen?Empfangen%i Nachrichten sind angekommenEmpfangene EreignisseEmpfangeEmpfange DateiEmpfУЄngerAktualisierenEntferntEntfernter PortEntfernte DateiУƒТМbertragungenEntfernte Station:EntfernenLУЖsche FilterUmbenennenAntwortenAuf gegenwУЄrtig ausgewУЄhlte Nachricht antwortenPositionsanfrageFrage alle Positionen abErbitte PostabfrageAngefordertes File %sAngeforderte Liste der DateienAngeforderte Nachricht %sAngeforderte Liste der NachrichtenErfordert einen Neustart von D-RATSGrУЖУŸe УЄndern aufNeustartWiederaufnehmen beiEmpfangenStarte TestLaufendSMTP PasswortSpeicher BildSpeichere Bild (sichtbarer Bereich)Linien des ZurУƒТМckrollensSekunden Wartezeit zwischen Zustellversuchen von weitergeleiteten Nachrichten. Nicht zu klein setzen!SendenSende D*QuerySende DateiSende Nachrichten im AusgangSenden УМber...Senden/EmpfangenGesendetSerielle SchnittstelleSerieller Anschluss fУƒТМr ein NMEA-kompatibles, externes GPSEinstellungenZeigenZeige Debug-LogZeige Ereignistyp:Zeige StationslisteZeige Statusneuigkeiten im ChatZeit in UTC anzeigenSeitenpinneNachricht beim AbmeldenNachricht beim AnmeldenDa es das erste Mal ist, dass Du D-RATS startest,Einsatzorte (durch Beistrich getrennt)GrУЖУŸeGrУЖУŸe (Bytes)KleinSocket-Sitzung %s begann mit %sKlУƒТЄngeAdresse der QuelleRufzeichen des SendersGib ein .WAV-File an, das gespielt wirdGrУЖУŸe zu BeginnStatische PositionTTL der StationStationskarteStation muss eine FlУЄche seinStationenStatistikBetreffTCP WeiterleitungTabelleTeste VerbindungTestparameterTyp des TestsZeichenkette als Antwort auf einen Ping.Der Standard von NMEA ist 4800Die Baudrate, mit der mit dem TNC kommuniziert wird (nicht die УМber Funk)Das Symbol fУƒТМr GPS-A BakenDas Symbol aus der Zeichentabelle fУƒТМr GPS-A BakenFУМr die Dongle-Benutzung gibt es keine ParameterDas ist die Nachricht, die anderen StationenDas ist der Status, die andere StationenDieses Dienstprogramm erlaubt es, D*Query-Kommandos an das lokale Gateway zu senden (wenn unterstУМtzt). Gib hier das Kommando ein (z.B. wx) und drУМcke Send. Wenn erfolgreich, erscheint im Chat-Fenster die Antwort.ZeitPosition des ZeitstempelsZeitstempel fУƒТМr ChatnachrichtenKnУƒТЖpfe der WerkzeugleisteGesamtMittelpunkt der SpurУœbertragung abgeschlossenУœbertragung unterbrochenУƒТœbertragungMУМllBehandle einlaufenden Rohtext (und MУƒТМll) als Zeichen und stelle sie am Bildschirm darUNBEKANNTVerbindung konnte nicht hergestellt werdenGPS Port konnte nicht geУЖffnet werdenKann Date %s: %s nicht УЖffnenУ–ffnen erfolglos: Nachricht wird von einem anderen Task benutztAdresse konnte nicht analysiert werdenSenden erfolglos: Nachricht wird von einem anderen Task benutztEinheitenUnbekanntUnbekanntes BildformatHochladenExternes GPS benutzenSSL benutzenUsername fУМr SMTP-Authentifizierung. Keine Authentifizierung, wenn leerVersions- und BetriebssysteminformationWL2K VerbindungWL2K Netzwerk ServerWarte auf ersten BlockWarte auf AntwortWarte auf УœbertragungsbeginnLУƒТЄnge der PrУƒТЄambelPrУƒТЄambel ZeitУƒТМberschreitungWetter (WU)Wenn aktiviert wird D-RATS die Pakete in AX.25 UI Frames verpacken, die einen Digipeater passieren kУЖnnenWenn aktiviert, Zeiteingaben werden auf aktuelle Uhrzeit in UTC voreingestellt, wenn deaktiviert, auf lokale Zeit.Musst Du ein Rufzeichen eingeben, bevor Du weitermachstDu muss ein Rufzeichen eingeben.Deine aktuelle HУƒТЖheDeine aktuelle Breite in Dezimalgraden (DD.DDDDD) oder D*M'S". Benutze ein Leerzeichen fУƒТМr spezielle ZeichenDeine aktuelle LУƒТЄnge in Dezimalgraden (DD.DDDDD) oder D*M'S". Benutze ein Leerzeichen fУƒТМr spezielle ZeichenabwesendBytesChatGradexistiert nicht. Willst Du es erzeugen?weiterleiten anvonStundenist jetztzuletzt gesehen umvon GrУЖУŸeauf PortSeite 1Seite 2Seite 3Seite 4Seite 5empfangenerhalten vonmeldendMail Check angefordertWiederholungensagtSekundengesendetd-rats-0.3.3/locale/en/000077500000000000000000000000001160617671700145645ustar00rootroot00000000000000d-rats-0.3.3/locale/en/LC_MESSAGES/000077500000000000000000000000001160617671700163515ustar00rootroot00000000000000d-rats-0.3.3/locale/en/LC_MESSAGES/D-RATS.mo000066400000000000000000000005721160617671700176440ustar00rootroot00000000000000о•$,8@9Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2011-01-01 14:08-0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit d-rats-0.3.3/locale/es/000077500000000000000000000000001160617671700145715ustar00rootroot00000000000000d-rats-0.3.3/locale/es/LC_MESSAGES/000077500000000000000000000000001160617671700163565ustar00rootroot00000000000000d-rats-0.3.3/locale/es/LC_MESSAGES/D-RATS.mo000066400000000000000000000777601160617671700176660ustar00rootroot00000000000000о•ѓДЁLа) б)л)є)!*5*,>*+k*—*Ј*К*Ъ*щ*я*ѕ*!ћ*+$+++ /+ :+E+ W+ b+m+”u+ ,,8,O,b,#u,#™,-Н,ы,1є,–&-Н-г- ч-.ђ- !.-.?. ^. h. v..†.™.­. Х.в.ъ.№.є.§. // /&/$5/Z/ _/ m/w/†/ ˜/IІ/,№/D0b0 ~0 ˆ0”00Ѕ0Ў0pЕ0&1 61C1K1a1 r11—1Ќ1Н1 Ф1 х1ѓ1њ1 2 22 2(22<2+o2›2 Ђ2%А2 ж2т2ї2 џ2 3 3K3j3s3z3ƒ3Š3’3Ђ3Ї3З3 Ш3 е3т3 ё34ў334 ;4&G4n4Œ4.4Ь4р4ђ455%)5O5l5Š5!Ђ5 Ф5Ю5г5к5 щ5і5# 6 /6P6U6h6 w6 …6#І6Ъ6г6<й6’7Љ7Т7Ч7ж7х7 э7љ7 8 88)8 /8 <8H8X8 k8 x8„8!›8Н8У8Ы8 а8‘н8o9pt99х97:=W:@•:jж: A; N;[;d;s;y;_“;ѓ;ќ;<<,<><M<^<|< <ž< А<Н<Ц<Ь<~е< T=b=i=q= w=‚=”=Є=Ј= Й=У=Ъ=м=п=,ф=>> &> 0>+<>-h>–>š> Ђ>Џ>С>&в>љ>? ? ???3?C?H?]?e?i?{?‹?0Є?е? к? ч?є?ј? @*@&=@d@v@†@˜@ @Ї@А@П@Ю@ ж@ т@ю@і@ џ@ A&A/A4A FA SA^AdA tA‚A ‡A•A›A ЌAИA РAЪAуAђAїA џA BB.B2B8BABSBbBkB€B BšB ЉBГBЛB ТBЮBфBєB ћB CC'C>COCeCxCŠCžCГCЪCфC ъCєC ќC DD'D0D 8D ED SD _DmD rD}D—DUЈDўD E EE6E ?ELESEXE_E E.‹EКEСEЪEЯEоEяEFF .F8FIF.YFˆF F ЅFВFИF!ОFрFчFіF G'G -G;GKG SG _GkG„GG –GЁGЈG­GЕGНG ЬGиGмGхGьGёGH HH,"HOHJiH&ДH,лH,I(5I&^Iв…IXJ]JqJ‰J™J ŸJЌJПJ дJоJKфJ0K5K=KOKgK.‚KБK.ЩKјKўKLL L1L9L4BLwL‹L›LЏLРLиLэL MM (Mm5MiЃM% N$3NXNenNfдN;O@OFOLOROXO]OcOjOoOuOzO*‚O ­OЛO РOЪO вO нO чO ёOќOP P P'P-P 4PAPIPQPXP_PfPmPtP }P ‹P•PЌPДPЙPСPEЦP RR'4R'\R„R,R,КRчRјR S%SCSISOS!USwS~S†SŽSSЎSЦSиS чS˜ђS ‹T–TU›TёTU*U1IUA{UНUCХUš VЄVЙV ЯV6кVWW/WOWWWiW{WW˜WЖWвWуWџWX XX(X1X 8XFX8WXX•XЄXГXЩXпXKѓX5?YGuYНY мY шYѕY ўY ZZr!Z”ZЉZКZУZиZ ыZјZ['[<["B[ e[r[y[[ ’[Ÿ[Ѕ[­[?Ш[8\A\ H\V\ u\‚\•\ ž\ Ј\ Г\TП\ ]]%] +]6]=]Q]X]o]‡] –] Ѓ] Б]*О]щ]ђ]1^+5^a^9w^Б^а^т^ќ^_10_b__›_&Е_м_ы_№_љ_ ``()`'R`z`&‚`Љ`У`4й`-ahEhLhЁThіhiii i,iEiYi]ioixii•i ˜i/Ђiвi$зiќij*%j-Pj~j‚jŠjšjАj-Цjєj§jk kk#k8kHkOkgkkkqk‰kžk;Мkјkll)l/lCl&Rl"ylœlБlУlжlпlцlэl§l m m m4m=m!Fmhmnmwm|m˜mЉmЛmСmвmюm ѕmn n !n .n ;nFn`nwn|n„n˜nЗnЩnЭn гnнnђno o!o 3o>o Qo \ofo mo"{ožoАoЗo Шo вo!мoўop!1pSplp‰pЂpПpмp тp юpљp qq *q 8q Cq Pq ^q lqyqqqЎqfРq'r.r=rLr jrvr …rr—rr Иr3Хrљrs ss.sFs)bsŒs  sЎsРs+гsџst$t4t=t!Mtotwt‰t(›tФtЭtсt ѕt џt uu >u Iu Tubuiunu su}u Œu˜u œuЇuЎuГuЪu лuхu*ыuvL0v/}v<­v,ъv%w-=wйkwExJx!fx#ˆxЌxВxУxлxѕxyX yfy kywy%Œy#Вy2жy" z0,z]z fzrzz“zЄz­zJЕz!{"{1{F{Y{t{'ˆ{А{Ш{ к{vц{›]|0љ|#*}N}g`}hШ}1~6~>~G~N~S~X~^~e~k~q~v~}~ š~Ї~ Њ~ И~ Ф~ б~ н~ ш~ ѓ~ў~ ! '3 I T ^ h r | † ™ Ѕ Абкпш|аЊ/ˆ˜sBгEпкщyЁ№@ІžОАС—Е"ы xЏѓ‹д>›ёC ‡яyIPWОьlv5НбЃMЭƒВ&Уу~ -ЊюŽpsгe7ПцoшgRзNoыЅБ%вЛK+:\V+3cн{XœO^ТNOјEл‡Яв щUv‘G}ŒГхф"wИЂ)4mяЖ ZЏ1rAЉƒ(qЫ'<Ћ2МLЖьДŸошр-Ў<Ž;7_ЁIaї^ЮœЧšj8 #„G–жBЅо2Уjрˆ Т[–ЗѓХS“] ”,эtЛ‰ХuўJІћН—иžДЫЌБTР>Q[пFЇЧ™erйhTЉ`эу  ЕЌn~ФАŸх‘МWчM†$=‚šФч9Р%‚HkЬ0!P€б­…A;њ$i:ю3hbШa!ЪЄuD­ DX`l/л”Vpb§“Rеq•ŠŠм_мgЯ€џ]а'KZљQLfЙк.YКжи6dеф& @ъkЩ)}Јђ‰wЇ…?•Щ{,†№Ўd1Ѓ0|ЈПЋ=’\Цtќ„FЬКzёUГСсИъ›Ш5YcЙmВ‹тCЪHЮіS8(ŒJ™4*Эн69 ’fzцЄтйсǘзnЦ?дєЂi*x .ѕђ# on port when a new chat arrives when new file activity occurs when new message activity occurs%s Color1200 2400 4800 9600 19200 38400 57600 1152009600 for mobile radios, 38400 for handheldsMy StatusParametersStationsD*Query ToolA B CAGWPEAX.25AX.25 Digi path (CALL1,CALL2,...)AccessActionAddAdd FilterAdd MarkerAdd Quick MessageAdd a portAdd filterAddressAfter this many hours, a map tile will be re-fetched, regardless of if it is already stored locally. 0 means never re-fetch. 720 hours is 30 days.AirportAllAll File Transfers Form Transfers Position Reports PingsAllow POP3 GatewayAllow WL2K GatewayAllow remote stations to pull filesAllow remote stations to pull formsAlternate location to store cached map imagesAltitudeAmount of fake data to send during a warmup cycleAmount of time to wait between transmissions in seconds (a positive number is a fixed delay, a negative value means 'randomly choose between 0 and X')An error has occurredAn error occurred: AppearanceAre you sure you want to clear your map cache?AttachmentsAttempts per sizeAutomatically forward messagesBaud RateBlink tray onBlock sizeBothBroadcast LocationBroadcast Text FileBroadcast this locationBroken ColorBuoys (comma separated)BytesCAPCallsignCallsign ColorCancelCenterCenter hereCenter on thisChannel names must be a single-word ChatChat activityChat fontCheck spellingChecking mail forChoose a formChoose a script to execute. The output will be used when the QST is sentChoose a station whose position will be sentChoose a text file. The contents will be used when the QST is sent.Choose the port where chat Clear AllClear CacheCommand:CommentCompleteConfigConfigure data paths below. This may include any number of serial-attached radios and network-attached proxies.Confirm addressConfirm exitConnectConnected to InternetContaining text:Control: ACKControl: END session %sControl: NEW sessionControl: UNKNOWNCreateCreate a new message for sendingCreate filterD-RATSD-RATS StartedDPRSDPRS messageDateDefaultDefault GPS commentDefault is 25. Set to the value given by your ISPDefault state for private flag on new formsDeleteDelete FilterDelete the currently selected messageDescriptionDestination CallsignDetailsDigi PathDirectionDisconnectDisplay information about packets seen that are destined for other stationsDistanceDongleDownloadDraftsEcho ofEcho request ofEditEdit Map SourceEdit Map SourcesEdit SourcesEmail AccessEmail AccountsEmail FilterEmail address to set on outgoing form email messagesEnabledEnding sizeEnter a WeatherUnderground station ID:Enter a filter search string:Enter a message:Enter an address, postal code, or intersectionEnter channel name:Enter destinationEnter filter textEnter remote callsignEnter station:Enter text for the new quick message:Enter the URL of a CAP feed:Enter the URL of an RSS feed:Enter your GPS message:Error starting socket session: %sEvent LogExecExportExport MessageExternal GPSFailed to connect toFailed to receive file (incomplete)Failed to send file (incomplete)FileFile Transfer PathFile TransfersFile activityFile is too large to send (>8KB)File transfer of %s started with %sFilenameFilesFor a network connection, use something like 'net:host:9000'For each position report recieved, change the callsign to 'callsign.datestamp'. NOTE: This will generate a LOT of map pointers, use with caution!Force transmission delayFormForm Logo PathForm TransfersForwardForward viaFreshen map afterGPSGPS LockedGPS Not LockedGPS-AGPS-A SymbolGPSd LockedGPSd Not LockedGPSd Socket closedGenerated atGet versionGoing offline (D-RATS)Gradually increasing packet sizesGroupHeadingHostHost AddressHostname of outgoing SMTP server. If this is specified, this station will be a gateway for email forms. If left blank, this feature is disabledIconIf a station was last heard more than this many seconds ago, do not assume you have a clear path (ping it first)If enabled, attempt to negotiate TLS/SSL with SMTP serverIf enabled, take current position from the external GPSIf prefixed by a ! character, interpret as a path to a scriptIf prefixed by a > character, interpret as a path to a text fileIf the TNC is a multi-port unit, this specifies which port will be used (starting with 0 as the first one)Ignore ColorIgnore RegExImperialImport MessageInboxInclude original in replyInclude the text of the original message when replying (not recommended as it wastes bandwidth)IncomingIncoming AccountsIncoming ColorIncoming FilesIncoming MessagesIncrement sizeInvalid GPS dataInvalid character in callsignInvalid port stringInvalid valueInvalid value forJoin ChannelLanguageLargeLatitudeLength of time between transmissions that must pass before we send a warmup block to open the power-save circuits on handheldsLoad log tailLoadedLoadingLocalLocal PortLocate an addressLocations foundLogLog chat trafficLongitudeLookupLookup by addressMEMainManually direct a message to another stationMapMap Storage PathMark ReadMark UnreadMark the currently selected message as readMark the currently selected message as unreadMaxMessageMessage SentMessage TemplatesMessage activityMessage transfer of %s started with %sMessagesMetricMinMiscMoreMultiple fixed-size packetsMy Winlink SSIDNameNegotiation CompleteNetworkNewNew Chat MessagesNew marker hereNo start block received!No support for resizing images. Send unaltered?NoneNotice ColorNotice RegExNowNumber of packetsOfflineOn UNIX, use something like '/dev/ttyUSB0'On Windows, use something like 'COM12'Online UnattendedOnline (D-RATS)Open Private ChatOptionsOutboxOutgoingOutgoing ColorOutgoing EmailOverlayPOP3 ServerPacket sizePacketsPasswordPassword for SMTP authenticationPathsPersonalPingPing All StationsPing StationPing replyPingsPipeline blocksPoll IntervalPortPort and ModePort:Position ReportsPreferencesPreviewPrintablePrintable (visible area)QST Size LimitQSTsQualityQuery UserQueue flush intervalQuick MessagesRSSRadioRaw TextReally delete %s?Really delete?ReceivedReceived %i messagesReceived EventsReceivingReceiving fileRecipientRefreshRemoteRemote PortRemote file transfersRemote station:RemoveRemove FilterRenameReplyReply to the currently selected messageRequest PositionRequest all positionsRequest mail checkRequested file %sRequested file listRequested message %sRequested message listRequires a D-RATS restartResetResize toRestartResuming atRetrievingRouting InformationRun TestRunningSMTP GatewaySMTP PasswordSMTP ServerSMTP UsernameSaveSave ImageSave Image (visible area)Scrollback LinesSeconds between each attempt to process forwarded messages. Do not set this too low!SendSend D*QuerySend fileSend messages in the OutboxSend viaSend/ReceiveSenderSentSerialSerial Network TNC Dongle AGWPESerial PortSerial port for an NMEA-compliant external GPSServerSettingsShowShow debug logShow event type:Show station listShow status updates in chatShow time in UTCSide PaneSign-off MessageSign-on MessageSince this is your first time running D-RATS, Sites (comma separated)SizeSize (bytes)SmallSniffSocket session %s started with %sSoundsSource AddressSource CallsignSpecify a .WAV file to be playedStartStarting sizeStatic positionStationStation TTLStation mapStation must be a plain Station:StationsStatisticsStatusStopSubjectSymbol:TCP ForwardingTCP GatewayTNCTNC PortTable:TestTest ConnectivityTest ParametersTest TypeTextText string to return in response to a ping.The NMEA standard is 4800The baud rate used to communicate with the TNC (not the rate over the air)The symbol character for GPS-A beaconsThe symbol table character for GPS-A beaconsThere are no parameters for dongle operationThis is the message other stations will This is the state other stations will This utility will allow you to send D*Query commands to the local gateway (if supported). Type a command here (such as wx) and hit Send. If successful, the result will appear in the chat window.TimeTimestamp PositionsTimestamp chat messagesToolbar buttonsTotalTrack centerTransfer CompletedTransfer InterruptedTransfersTrashTreat incoming raw text (and garbage) as chat data and display it on-screenTypeUNKNOWNUnable to connectUnable to open GPS portUnable to open file %s: %sUnable to open: message in use by another taskUnable to parse addressUnable to send: message in use by another taskUnitsUnknownUnknown image typeUploadUse External GPSUse SSLUsernameUsername for SMTP authentication. Disabled if blankVersion and OS InfoWL2K ConnectionWL2K Network ServerWL2K RMS StationWaiting for first blockWaiting for responseWaiting for transfer to startWarmup LengthWarmup timeoutWeather (WU)When enabled, D-RATS will encapsulate its packets in AX.25 UI frames which can be passed through a digipeaterWhen enabled, form time fields will default to current time in UTC. When disabled, default to local timeYou must enter a callsign to continueYou must enter a station callsign. Your current altitudeYour current latitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersYour current longitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersZoom_Edit_File_Help_Viewa b a b ca b c awaybyteschatdegreesdoes not exist. Do you want to create it?forwarding tofromgtk-aboutgtk-addgtk-cancelgtk-cleargtk-closegtk-deletegtk-editgtk-okgtk-preferencesgtk-removehoursis nowlast seen atof sizeon portpage 1page 2page 3page 4page 5receivedreceived fromreportingrequested a mail checkretriessayssecondssentProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2011-01-01 14:08-0800 PO-Revision-Date: 2010-12-30 08:00+0100 Last-Translator: IУБigo Bastarrika EA2CQ Language-Team: Language: Spanish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en puerto cuando llega un nuevo chat cuando hay nueva actividad de ficheros cuando hay nueva actividad de mensajesColor %s1200 2400 4800 9600 19200 38400 57600 1152009600 para radios mУГviles, 38400 para talkisMi EstadoParУЁmetrosEstacionesHerramienta D*QueryA B CAGWPEAX.25Ruta Digi AX.25 (CALL1,CALL2,...)AccesoAcciУГnAУБadirAУБadir FiltroAУБadir MarcadorAУБadir Mensaje RУЁpidoAУБadir un puertoAУБadir filtroDirecciУГnTras estas horas, se cargarУЁ un copia del mapa, independientemente de si estУЁ guardado localmente. 0 signfica no cargar nunca. 720 horas son 30 dУ­as.AeropuertoTodoTodo Trasferencia de Ficheros Trasferencia de Formularios Reportes de PosiciУГn PingsPermitir Gateway POP3Permitir Gasteway WL2KPermite a estaciones remotas usar ficherosPermite a estaciones remotas utilizar formulariosLugar alternartivo para guardar las imУЁgenes de mapas del cachУЉAltitudCantidad de datos falsos a enviar durante el ciclo de calentamientoTiempo de espera entre transmisiones en segundos (un nУКmero positivo es un tiempo fijo, un valor negativo indica 'seleccionado aleatoriamente entre 0 X')Ha ocurrido un errorHa ocurrido un error:AparienciaEstУЁs seguro de que quieres borrar tu cachУЉ de mapa?AdjuntosIntentos por tamaУБoForward automУЁtico de mensajesBaudiosBarra parpadeanteTamaУБo de bloqueAmbosDifundir emplazamientoFichero de Texto de BroadcastDifundir este emplazamientoColor de ErroresBoyas (separadas por comas)BytesCAPIndicativoColor de IndicativoCancelarCentroCentrar aquУ­Centrar en УЉsteLos nombres de los canales deben de ser una sola palabraChatActividad chatFuente de chatComprobar ortografУ­aComprobando mail paraElije un formularioSelecciona un script para ejecutar. El resultado se usarУЁ al enviar el QSTSelecciona una estaciУГn cuya posiciУГn serУЁ enviadaSelecciona un fichero de texto. El contenido se usarУЁ al enviar el QSTElije el puerto donde el chat Borrar TodoBorrar CacheComando:ComentarioCompletoConfiguraciУГnConfigura la ruta de datos. Esto puede incluir cualquier nУКmero de radios conectadas vУ­a serie y proxies de red.Confirmar direcciУГnConfirmar salidaConectarConectado a InternetContiene el texto:Control: ACKControl: FIN sesiУГn %sControl: NUEVA sesiУГnControl: DESCONOCIDOCrearCrear un nuevo mensaje para enviarCrear filtroD-RATSHa arrancado D-RATSDPRSMensaje DPRSFechaDefectoComentario GPS por defectoPor defecto es 25. Selecciona el valor proporcionado por tu ISPEstado por defecto de flag privado en nuevos formulariosBorrarBorrar FiltroBorrar el mensaje seleccionadoDescripciУГnIndicativo DestinoDetallesRuta DigiDirecciУГnDesconectarMuestra la informaciУГn de paquetes detectados que son destinados a otras estacionesDistanciaDongleBajarBorradoresEco deSolicitud de eco deEditarEditar Fuente de MapasEditar Fuentes de MapasEditar FuentesAcceso EmailCuentas EmailFiltro EmailDirecciУГn email para los emails salientesActivadoTamaУБo de finalIntroduce una ID de estaciУГn WeatherUnderground:Introduce la cadena de bУКsqueda de filtro:Introduce un mensaje:Introduce una direcciУГn, cУГdigo postal, o intersecciУГnIntroduce el nombre del canal:Introduce destinoIntroduce texto de filtroIntroduce el indicativo remotoIntroduce estaciУГn:Introduce el texto para el nuevo mensaje rУЁpido:Introduce la URL the un CAP:Introduce la URL de un RSS:Introduce tu mensaje GPS:Error al comenzar la sesion socket: %sLog de EventosEjecExportarExportar MensajeGPS ExternoFallo conectando aFallo al recibir el fichero (incompleto)Fallo al enviar el fichero (incompleto)FicheroRuta para la Transferencia de FicherosTrasferencias de FicherosActividad de ficherosEl fichero es demasidado grande para enviarlo (>8KB)Trasferencia de fichero de %s comenzУГ con %sNombre de FicheroFicherosPara una conexiУГn de red, serУЁ del tipo 'net:host:9000'Por cada reporte de posiciУГn recibido, cambiar el indicativo a 'indicativo.sello fecha'. NOTA: Esto generarУЁ muchos marcadores de mapa, УКsalo con precauciУГn!Forzar retardo de la transmisiУГnFormularioRuta para el Logo del FormularioTrasferencias de FormulariosForwardForward vУ­aRefresco de mapa trasGPSGPS BloqueadoGPS DesbloqueadoGPS-ASУ­mbolo GPS-AGPSd BloqueadoGPSd No BloqueadoSocket GPSd cerradoGenerado enObtener versiУГnFuera de linea (D-RATS)Gradualmente incrementando el tamaУБo de los paquetesGrupoDirecciУГnHostDirecciУГn HostServidor de salida SMTP. Si se especifica, esta estaciУГn serУЁ un gateway para formularios email. Si estУЁ en blanco, esta caracterУ­stica queda desactivadaIconoSi una estaciУГn fue escuchada por УКltima vez hace mУЁs de estos segundos, no asumir que la ruta estУЁ abierta (hacer ping primero)Si estУЁ activado, intenta negociar TLS/SSL con el servidor SMTPSi estУЁ activado, toma la posiciУГn actual del GPS externoSi estУЁ precedido por un carУЁcter !, interpretar como una ruta a un scriptSi estУЁ precedido por un carУЁcter >, interpretar como una ruta a un fichero de textoSi la TNC es multi-puerto, aquУ­ se indica quУЉ puerto se usarУЁ (el primero comienza por 0)Ignorar ColorIgnorar Reglas para Aviso (regEx)AnglosajУГnImportar MensajeEntradaIncluir original en la respuestaIncluir el texto del mensaje original al responder (no recomendado porque desperdicia ancho de banda)EntradaCuentas de EntradaColor en EntradaFicheros EntrantesMensajes EntrantesIncrementar el tamaУБoDatos GPS invУЁlidosCarУЁcter invУЁlido en el indicativoCadena de puerto invУЁlidaValor invУЁlidoValor invУЁlido paraUnirse al CanalIdiomaGrandeLatitudDuraciУГn de tiempo entre transmisiones que debe de pasar antes de enviar un bloque de calentamiento para abrir los circuitos de ahorro de energУ­a de los talkisCarga cola de logCargadoCargandoLocalPuerto LocalLocalizar una direcciУГnLugares encontradasLogLog trУЁfico chatLongitudBuscarBuscar por direcciУГnYOPrincipalDirigir un mensaje manualmente a otra estaciУГnMapaRuta para el Almacenamiento de MapasMarcar como LeУ­doMarcar como No LeУ­doMarcar el mensaje seleccionado como leУ­doMarcar el mensaje seleccionado como no leУ­doMaxMensajeMensaje EnviadoPlantillas de MensajeActividad de mensajesTrasferencia de mensaje de %s comenzУГ con %sMensajesMУЉtricoMinMiscMУЁsMУКltiples paquetes de tamaУБo fijoMi SSID WinlinkNombreNogociaciУГn CompletadaRedNuevoNuevos Mensajes de ChatNuevo marcador aquУ­Bloque de inicio no recibido!Reescaldado de imУЁgenes no soportado. Enviar sin cambios ?NingunoColor en AvisoReglas para Aviso (RegEx)AhoraNУКmero de paquetesFuera de lineaEn UNIX, serУЁ del tipo '/dev/ttyUSB0'En Windows, serУЁ del tipo 'COM12'En Linea No atendidoEn Linea (D-RATS)Abrir Chat PrivadoOpcionesSalidaSalidaColor en SalidaEmail de SalidaCapaServidor POP3TamaУБo del paquetePaquetesPasswordPassword para autenticaciУГn SMTPRutasPersonalPingPing a Todas las EstacionesPing a EstaciУГnRespuesta al pingPingsBloques pipelineIntervalo de InterrogaciУГnPuertoPuerto y ModoPuerto:Reportes de PosiciУГnPreferenciasVista PreviaImprimibleImprimible (area visible)LУ­mite de tamaУБo QSTQSTsCalidadUsuario solicitanteIntervalo de solicitud de colaMensajes RУЁpidosRSSRadioTexto RawRealmente borrar %s?Seguro borrar ?RecibidoRecibidos %i mensajesEventos RecibidosRecibiendoRecibiendo ficheroRecipienteRefrescarRemotoPuerto RemotoTransferencias de ficheros remotosEstaciУГn remota:QuitarQuitar el FiltroRenombrarResponderResponder al mensaje seleccionadoSolicitar PosiciУГnSolicitar todas las posicionesSolicitar comprobaciУГn de correoSolicitado el fichero %sLista de ficheros solicitadaSolicitado el mensaje %sLista de mensajes solicitadaSe requiere reiniciar D-RATSResetReescalar aRecomenzarRecomenzando enRecuperandoInformaciУГn de rutaEjecutar TestEjecutandoGateway SMTPPassword SMTPServidor SMTPUsuario SMTPGuardarGuardar ImagenGuardar Imagen (area visible)Lineas ScrollbackSegundos entre cada intento de procesar los mensajes forwardeados. No indicar un valor demasiado bajo!EnviarEnviar D*QueryEnviar ficheroEnviar los mensajes en SalidaEnviar vУ­aEnviar/RecibirRemitenteEnviadoSerieSerie Red TNC Dongle AGWPEPuerto SeriePuerto serie para un GPS externo con protocolo NMEAServidorOpcionesMostrarMuestra log de depuraciУГnMuestra tipo de evento:Muestra lista de estacionesMuestra actualizaciones de estado en chatMuestra hora en UTCPanel LateralMensaje de CierreMensaje de EntradaComo es la primera vez que ejecutas D-RATS,Sitios (separados por comas)TamaУБoTamaУБo (bytes)PequeУБoHusmear (sniff)SesiУГn Socket %s comenzУГ con %sSonidosDirecciУГn OrigenIndicativo OrigenEspecificar un fichero .WAV a reproducirComienzoTamaУБo de comienzoPosiciУГn estУЁticaEstaciУГnEstaciУГn TTLMapa de estaciУГnLa estaciУГn debe ser EstaciУГn:EstacionesEstadУ­sticasEstadoStopTemaSУ­mbolo:Forwarding TCPGateway TCPTNCPuerto TNCTabla:TestComprobar ConectividadParУЁmetros TestTipo TestTextoTexto que se envУ­a en respuesta a un pingEl estУЁndar NMEA es 4800Los baudios para comunicar con la TNC (no la velocidad comunicaciУГn por RF)El carУЁcter de sУ­mbolo para las balizas GPS-AEl carУЁcter de la tabla de sУ­mbolos para las balizas GPS-ANo hay parУЁmetros para operar con el dongleEste el mensaje que otras estaciones Este es el estado en el que otras estaciones Esta utilidad te permite enviar comandos D*Query al gateway local(si estУЁ soportado). Escribe aquУ­ un comando (p.e. wx) y pulsa Enviar. Si es correcto, el resultado aparecerУЁ en la pantalla de chat.HoraSello de hora en PosicionesSello de Hora en mensajes de chatBotones de la barra de herramientasTotalCentrar en TrackTrasferencia CompletadaTrasferencia InterrumpidaTransferenciasPapeleraTratar el texto raw the entrada (y la basura) como datos de chat y mostrarlo en pantallaTipoDESCONOCIDONo se puede conectarNo es posible abrir el puerto del GPSNo se puede abrir el fichero %s: %sNo se puede abrir: mensaje en uso por otro procesoNo se puede procesar la direcciУГnNo puedo enviar: mensaje en uso por otro procesoUnidadesDesconocidoTipo de imagen desconocidoSubirUsar GPS ExternoUsar SSLUsuarioNombre de usuario para autenticaciУГn SMTP. Desactivado si estУЁ en blancoInfo Sistema Operativo y VersiУГnConexiУГn WL2KServidor de Red WL2KEstaciУГn RMS WL2KEsperando al primer bloqueEsperando respuestaEsperando la trasferencia para comenzarDuraciУГn calentamientoFin calentamientoTiempo (WU)Cuando estУЁ activado, D-RATS encapsularУЁ sus paquetes en tramas UI AX.25 que pueden pasar a travУЉs de un digipeaterCuando estУЁ activado, los campos horarios del formulario serУЁn por defecto la hora actual UTC. Cuando estУЉ desactivado, serУЁn por defecto la hora localDebes de introducir un indicativo para continuarDebes de introducir un indicativo. Tu altitud actualTu latitud actual. Usa grados decimales (DD.DDDDD) o D*M'S". Usa un espacio para caracteres especialesTu longitud actual. Usa grados decimales (DD.DDDDD) o D*M'S". Usa un espacio para caracteres especialesZoom_Editar_Fichero_Ayuda_Vera b a b ca b c lejosbyteschatgradosno existe. Quieres crearlo ?forwarding adeacerca de gtkaУБadir gtkcancelar gtklimpiar gtkcerrar gtkborrar gtkeditar gtkgtk okpreferencias gtkquitar gtkhorasestУЁ ahoravisto por УКltima vezde tamaУБoen puertopУЁgina 1pУЁgina 2pУЁgina 3pУЁgina 4pУЁgina 5recibidorecibido dereportandosolicitada comprobaciУГn de mailintentosdicesegundosenviadod-rats-0.3.3/locale/it/000077500000000000000000000000001160617671700145765ustar00rootroot00000000000000d-rats-0.3.3/locale/it/LC_MESSAGES/000077500000000000000000000000001160617671700163635ustar00rootroot00000000000000d-rats-0.3.3/locale/it/LC_MESSAGES/D-RATS.mo000066400000000000000000000754431160617671700176670ustar00rootroot00000000000000о•э„“ь8) 9)C)\)!{)),І)+г)џ)*"*2*Q*W*]*d*k* o* z*…* —* Ђ*­*”Е*J+R+8V++Ђ+#Е+#й+-§++,14,–f,§,- '-.2- a-m-- ž- Ј- Ж-С-Ц-й-э- ..*.0.4.=.L.S. Z.f.$u.š. Ÿ. ­.З.Ц. и.Iц.,0/D]/Ђ/ О/ Ш/д/н/х/ю/pѕ/f0 v0ƒ0‹0Ё0 В0П0з0ь0§0 1 %131:1I1 N1[1`1h12|1+Џ1л1 т1%№1 2"272 ?2 I2 S2K^2Њ2Г2К2У2Ъ2в2т2ч2ї2 3 3"3 134>3s3 {3&‡3Ў3Ь3.н3 4 424D4Z4%i44Ќ4Ъ4!т4 5555 )565#K5 o55•5Ј5 З5 Х5#ц5 66<6’V6щ6777%7 -797K7 O7Z7i7 o7 |7ˆ7˜7 Ћ7 И7Ф7!л7§78 8 8‘8Џ8pД89%97_9=—9@е9 : #:0:9:H:N:_h:Ш:б:у:ђ:;;";3;Q; e;s; …;’;›;Ё;~Њ; )<7<><F< L<W<i<y<}< Ž<˜<Ÿ<Б<Д<,Й<ц<ъ< ћ< =+=-==k=o= w=„=–=&Ї=Ю=з=о=т=ч=ь=>>>2>:>>>P>`>0y>Њ> Џ> М>Щ>Э>п>*ч>&?9?K?[?m?u?|?…?”?Ѓ? Ћ? З?У?Ы? д?ѕ?ћ?@ @ @ (@3@9@ I@W@ \@j@p@ @@ •@Ÿ@И@Ч@Ь@ д@п@є@AA AA(A7A@A PAZA iAsA{A ‚AŽAЄAДA ЛAЩAаA'жAўAB%B8BJB^BsBŠBЄB ЊBДB МB ШBгBчB№B јB C C C-C 2C=CWCUhCОC УC аCкCіC џC DDDD ?D.KDzDDŠDDžDЏDСDнD юDјD E.EHE`E eErExE!~E EЇEЖE ЦEчE эEћE F F F+FDFMF VFaFhFmFuF}F ŒF˜FœFЅFЌFБFУF гFнF,тFG&)G,PG,}G(ЊG&гGвњGЭHвHцHўHI I!I4I IISIKYIЅIЊIВIФIмI.їI&J.>JmJsJ{JŽJ•JІJЎJ4ЗJьJKK$K5KMKbK €KŽK KiЊK%L$:L_LeuLfлLBMGMMMSMYM_MdMjMqMvM|MM*‰M ДMТM ЧMбM йM фM юM јMN NN #N.N4N ;NHNPNXN_NfNmNtN{N „N ’NœNГNЛNРNШN ЭN кOцOџOP 0P,:P)gP‘PЃPДP!ФPцPьPђPњPQ QQ*Q@Q SQ _QiQ RRJRcRyR'R+ЗR1уR S4 SЌUSTT9T7ATyT‚T›TЛTеTшTњTUU0UKUbU~U„U ˆU“UЅU­U ДUПU1гUV VV3VHV\VMnV4МVGёV9WYWiWWŠW“W œW€ІW'XvXvhvxv €v8Œv!Хvчvјv ww5wKwiw{w ‘wŠœw%'x"Mxpx~ƒxy‚y ‡y‘y—y žyЊyЏyЕyМyРyЦyЫyбy ьyіy љyz z z z *z5z>zEz Uz`z dznzz ’zžzЇzАzЙzТzЫz дz рzъz{ {{{WРЋRХNdupuЪIЩ{й“Е›С%X…‡еЦЈŒ 8шJГŒлЖœ>ЬОтjЦѕaq5|дЅ )бФЇiП–&žiKд 8(AзZ4*ХџЩШЅеgгн”СЂlн{\ђЮnJE&р dh`oцfODц<’Žv1mO/ƒ€ЮYМЗˆpє­9h^Ж4"вn˜Т.Ы™7№W‹П'чyЪƒ„ьА|,яујэ#ўŠ=Я‚УйmŽг‚ъЉvУ ‘XBЕt”Qу6О—ЗоЏkBœ(+сsКы3`ИИЇM†Nкм… ЎАЧ9вUћЋІP[ *ЌЎbрЭмф_;т‰НиšхwЫ~—пc^FЧMКЏаtVЙ•CjбЊLЌВ 7Ш2іЂДCЈ,жˆ0ZыёIЛ‡ДаzŸ“ЄзФ›ЬSЛищ<wѓБPyъLx˜sлю_.[?™:  TG2q!І5Тc-e HVAќ%Њ]Бg+1G­E§x@;}o„}H=НŠY’3ŸГщr шхšЁ!ЉTЙп\? @0FЯU'":Єeљa-сDфВrSžњк$$ь ЃK/жэ‰їРzМ6ЭоR‹€#]b†Ѓ•QЁkl‘~–>)чf on port when a new chat arrives when new file activity occurs when new message activity occurs%s Color1200 2400 4800 9600 19200 38400 57600 1152009600 for mobile radios, 38400 for handheldsMy StatusParametersStationsD*Query ToolA B CAGWPEAccessActionAddAdd FilterAdd MarkerAdd Quick MessageAdd a portAdd filterAddressAfter this many hours, a map tile will be re-fetched, regardless of if it is already stored locally. 0 means never re-fetch. 720 hours is 30 days.AirportAllAll File Transfers Form Transfers Position Reports PingsAllow POP3 GatewayAllow WL2K GatewayAllow remote stations to pull filesAllow remote stations to pull formsAlternate location to store cached map imagesAltitudeAmount of fake data to send during a warmup cycleAmount of time to wait between transmissions in seconds (a positive number is a fixed delay, a negative value means 'randomly choose between 0 and X')An error has occurredAn error occurred: AppearanceAre you sure you want to clear your map cache?AttachmentsAttempts per sizeAutomatically forward messagesBaud RateBlink tray onBlock sizeBothBroadcast LocationBroadcast Text FileBroadcast this locationBroken ColorBuoys (comma separated)BytesCAPCallsignCallsign ColorCancelCenterCenter hereCenter on thisChannel names must be a single-word ChatChat activityChat fontCheck spellingChecking mail forChoose a formChoose a script to execute. The output will be used when the QST is sentChoose a station whose position will be sentChoose a text file. The contents will be used when the QST is sent.Choose the port where chat Clear AllClear CacheCommand:CommentCompleteConfigConfigure data paths below. This may include any number of serial-attached radios and network-attached proxies.Confirm addressConfirm exitConnectConnected to InternetContaining text:Control: ACKControl: END session %sControl: NEW sessionControl: UNKNOWNCreateCreate a new message for sendingCreate filterD-RATSD-RATS StartedDPRSDPRS messageDateDefaultDefault GPS commentDefault is 25. Set to the value given by your ISPDefault state for private flag on new formsDeleteDelete FilterDelete the currently selected messageDescriptionDestination CallsignDetailsDigi PathDirectionDisconnectDisplay information about packets seen that are destined for other stationsDistanceDongleDownloadDraftsEcho ofEcho request ofEditEdit Map SourceEdit Map SourcesEdit SourcesEmail AccessEmail AccountsEmail FilterEmail address to set on outgoing form email messagesEnabledEnding sizeEnter a WeatherUnderground station ID:Enter a filter search string:Enter a message:Enter an address, postal code, or intersectionEnter channel name:Enter destinationEnter filter textEnter remote callsignEnter station:Enter text for the new quick message:Enter the URL of a CAP feed:Enter the URL of an RSS feed:Enter your GPS message:Error starting socket session: %sEvent LogExecExportExport MessageExternal GPSFailed to connect toFailed to receive file (incomplete)Failed to send file (incomplete)FileFile Transfer PathFile TransfersFile activityFile is too large to send (>8KB)File transfer of %s started with %sFilenameFilesFor a network connection, use something like 'net:host:9000'For each position report recieved, change the callsign to 'callsign.datestamp'. NOTE: This will generate a LOT of map pointers, use with caution!Force transmission delayFormForm Logo PathForm TransfersForwardForward viaFreshen map afterGPSGPS LockedGPS Not LockedGPS-AGPS-A SymbolGPSd LockedGPSd Not LockedGPSd Socket closedGenerated atGet versionGoing offline (D-RATS)Gradually increasing packet sizesGroupHeadingHostHost AddressHostname of outgoing SMTP server. If this is specified, this station will be a gateway for email forms. If left blank, this feature is disabledIconIf a station was last heard more than this many seconds ago, do not assume you have a clear path (ping it first)If enabled, attempt to negotiate TLS/SSL with SMTP serverIf enabled, take current position from the external GPSIf prefixed by a ! character, interpret as a path to a scriptIf prefixed by a > character, interpret as a path to a text fileIgnore ColorIgnore RegExImperialImport MessageInboxInclude original in replyInclude the text of the original message when replying (not recommended as it wastes bandwidth)IncomingIncoming AccountsIncoming ColorIncoming FilesIncoming MessagesIncrement sizeInvalid GPS dataInvalid character in callsignInvalid port stringInvalid valueInvalid value forJoin ChannelLanguageLargeLatitudeLength of time between transmissions that must pass before we send a warmup block to open the power-save circuits on handheldsLoad log tailLoadedLoadingLocalLocal PortLocate an addressLocations foundLogLog chat trafficLongitudeLookupLookup by addressMEMainManually direct a message to another stationMapMap Storage PathMark ReadMark UnreadMark the currently selected message as readMark the currently selected message as unreadMaxMessageMessage SentMessage TemplatesMessage activityMessage transfer of %s started with %sMessagesMetricMinMiscMoreMultiple fixed-size packetsMy Winlink SSIDNameNegotiation CompleteNetworkNewNew Chat MessagesNew marker hereNo start block received!No support for resizing images. Send unaltered?NoneNotice ColorNotice RegExNowNumber of packetsOfflineOn UNIX, use something like '/dev/ttyUSB0'On Windows, use something like 'COM12'Online UnattendedOnline (D-RATS)Open Private ChatOptionsOutboxOutgoingOutgoing ColorOutgoing EmailOverlayPOP3 ServerPacket sizePacketsPasswordPassword for SMTP authenticationPathsPersonalPingPing All StationsPing StationPing replyPingsPipeline blocksPoll IntervalPortPort and ModePort:Position ReportsPreferencesPreviewPrintablePrintable (visible area)QST Size LimitQSTsQualityQuery UserQueue flush intervalQuick MessagesRSSRadioRaw TextReally delete %s?Really delete?ReceivedReceived EventsReceivingReceiving fileRecipientRefreshRemoteRemote PortRemote file transfersRemote station:RemoveRemove FilterRenameReplyReply to the currently selected messageRequest PositionRequest all positionsRequest mail checkRequested file %sRequested file listRequested message %sRequested message listRequires a D-RATS restartResetResize toRestartResuming atRetrievingRouting InformationRun TestRunningSMTP GatewaySMTP PasswordSMTP ServerSMTP UsernameSaveSave ImageSave Image (visible area)Scrollback LinesSeconds between each attempt to process forwarded messages. Do not set this too low!SendSend D*QuerySend fileSend messages in the OutboxSend viaSend/ReceiveSenderSentSerialSerial Network TNC Dongle AGWPESerial PortSerial port for an NMEA-compliant external GPSServerSettingsShowShow debug logShow event type:Show station listShow status updates in chatShow time in UTCSide PaneSign-off MessageSign-on MessageSince this is your first time running D-RATS, Sites (comma separated)SizeSize (bytes)SmallSniffSocket session %s started with %sSoundsSource AddressSource CallsignSpecify a .WAV file to be playedStartStarting sizeStatic positionStationStation TTLStation mapStation must be a plain Station:StationsStatisticsStatusStopSubjectSymbol:TCP ForwardingTCP GatewayTNCTNC PortTable:TestTest ConnectivityTest ParametersTest TypeTextText string to return in response to a ping.The NMEA standard is 4800The symbol character for GPS-A beaconsThe symbol table character for GPS-A beaconsThere are no parameters for dongle operationThis is the message other stations will This is the state other stations will This utility will allow you to send D*Query commands to the local gateway (if supported). Type a command here (such as wx) and hit Send. If successful, the result will appear in the chat window.TimeTimestamp PositionsTimestamp chat messagesToolbar buttonsTotalTrack centerTransfer CompletedTransfer InterruptedTransfersTrashTreat incoming raw text (and garbage) as chat data and display it on-screenTypeUNKNOWNUnable to connectUnable to open GPS portUnable to open file %s: %sUnable to open: message in use by another taskUnable to parse addressUnable to send: message in use by another taskUnitsUnknownUnknown image typeUploadUse External GPSUse SSLUsernameUsername for SMTP authentication. Disabled if blankVersion and OS InfoWL2K ConnectionWL2K Network ServerWL2K RMS StationWaiting for first blockWaiting for responseWaiting for transfer to startWarmup LengthWarmup timeoutWeather (WU)When enabled, form time fields will default to current time in UTC. When disabled, default to local timeYou must enter a callsign to continueYou must enter a station callsign. Your current altitudeYour current latitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersYour current longitude. Use decimal degrees (DD.DDDDD) or D*M'S". Use a space for special charactersZoom_Edit_File_Help_Viewa b a b ca b c awaybyteschatdegreesdoes not exist. Do you want to create it?forwarding tofromgtk-aboutgtk-addgtk-cancelgtk-cleargtk-closegtk-deletegtk-editgtk-okgtk-preferencesgtk-removehoursis nowlast seen atof sizeon portpage 1page 2page 3page 4page 5receivedreceived fromreportingrequested a mail checkretriessayssecondssentProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2011-01-01 14:08-0800 PO-Revision-Date: Last-Translator: IZ5FSA Leo Language-Team: Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sulla portaquando arriva nuova chatquando arriva un filequando arriva un messaggioColore %s1200 2400 4800 9600 19200 38400 57600 1152009600 per i veicolari, 38400 per i palmariCondizioniParametriStazioniComandi D*QueryA B CAGWPEAccessoAzioneAggiungiCrea FiltroAggiungi SegnapostoCrea Messaggio RapidoAggiungi una portaCrea FiltroIndirizzoDopo questo numero di ore, la mappa sarУ  aggiornata, indipendentemente dal suo salvataggio locale. 0 significa nessun aggiornamento. 720 ore sono 30 giorni.AereoportoTuttiTutto Trasferimento File Trasferimento Messaggi Rapporto di Posizione PingGateway POP3 permessoPermetti WL2K GatewayAbilita stazioni remote a prendere fileAbilita stazioni remote a prendere messaggiPosizoine alternativa per le immagini delle mappeAltitudineQuantitУ  di dati fittizi per il messaggio di warmupQuantitУ  di secondi da attendere fra le trasmissioni (un valore positivo УЈ considerato un periodo fisso, negativo significa un tempo variabile fra 0 e il valore indicato)Si УЈ verificato un erroreSi УЈ verificato un errore:AspettoSei sicuro di coler cancellare la mappa dalla memoria ?AllegatiTentativi per dimensioneInoltro automatico dei messaggiVelocitУ  di TrasmissioneBarra lampeggianteDimensione bloccoentrambiPosizone di TrasmissioneInvia File di TestoTrasmetti questa posizioneColore Errore/CorrottoBuoys (separati da virgola)BytesCAPNominativoColore NominativoAnnullaCentroCentra quiCentra questo puntoI nomi dei canali devono essere una parola interaChatAttivitУ  su ChatFormato carattere chatControllo ortografiaControllo posta perScegli un modelloScegli uno script da eseguire. Il risultato sarУ  inviato come messaggio QST.Scegli la stazione per la quale inviare la posizioneScegli un file di testo. Il contenuto sarУ  inviato come messaggio QST.Scegli la porta dove 'chattare'Pulisci i campiPulisci Memoria (cache)Comando:CommentoCompletaConfiguraConfigura i sottostanti percorsi per i dati. Questo puo includere numero di radio conesse via seriale e poxy collegati via rete.Conferma l'indirizzoConferma l'uscitaConnettiConnesso a InternetContenente testo:Controllo: Acquisito (controllo ACK)Controllo: FINE sessine %sControllo: NUOVA sessioneControllo: SCONOSCIUTOCreaCrea un nuovo messaggio da spedireCrea filtroD-RATSD-RATS AvviatoDPRSMessaggio DPRSDatadefaultCommento GPS (default)Default a 25. Assegnare il valore indicato dal provider.Stato di default privato per i nuovi messaggiEliminaCancella FiltroCancella il messaggio selezionatoDescrizioneStazione DestinatariaDettagliPercorso del ripetitoreDirezioneDisconnettiVisualizza informazioni sui pacchetti ascoltati che sono destinati ad altre stazioniDistanzaDongleDownloadBozzaEco diRichiesta eco diModificaModifica sorgente mappeModifica sorgenti mappeModifica i datiAccesso EmailUtenze di postaFiltro emailIndirizzo email da assegnare per i messaggi in uscitaAbilitatoDimensione finaleDigita un ID Stazione Meteo (WU):Inserisci stringa di ricerca per filtro:Inserire un messaggio:Inserisci indirizzo, CAP o localitУ Digitare Nome canale:Inserire destinazioneInserisci testo per filtroInserisci nominativo remotoDigitare stazione:Inserisci il testo per il nuovo messaggio rapido:Inserire indirizzo (URL) del CAP:Inserire indirizzo (URL) del RSS:Inserire messaggio del GPS:Errore avvio sessione soket: %sLog eventiEseguiEsportaEsporta MessaggioGPS esternoImpossibile connettereMancata ricezione file (incompleta)Mancato invio file (incompleto)FilePercorso Trasferimenti FileTrasferimenti FileAttivitУ  su fileFile troppo grande per l'invio (>8kB)Trasferimento file di %s avviato con %sNome FileFilesPer connessioni via rete, usare il formato 'net:host:9000'Per ogni posizione ricevuta, cambia il nominativo in 'nominativo.dataora'. NOTA: Questo genererУ  molte posizioni sulla mappa, usare con cautela!Forzare il tempo di trasmissioneModelloPercorso Logo del ModelloTrasferimenti MessaggiInoltroInoltro viaAggiorna la mappa dopoGPSGPS BloccatoGPS Non BloccatoGPS-ASimbolo GPS-ASocket GPSd BloccatoSocket GPSd Non BloccatoSocket GPSd chiusoGenerato ilOttieni versioneDisconnesso (D-RATS)Incremento graduale dimensioni del pacchettoGruppoDirezioneHostIndirizzo HostHostname per server SMTP in uscita. Se specificato, questa stazione sarУ  gateway per invio e-mail. Se vuoto, la caratteristica sarУ  disabilitataIconaSe una stazione non viene ascoltata per piУЙ di questo tempo, si ritiene non sia piu attiva. Consigliato ping di verifica.Se abilitato, tenta di negoziare una connessione TLS/SSL son il server SMTPSe abilitato, prende la posizione corrente dal GPSSe preceduto da un "!", sarУ  interpretato come il percorso di uno scriptSe preceduto da uno ">", sarУ  interpretato come il persorso a un file di testoColore IgnoraRegole per Ignorare(RegEx)AnglosassoneImporta MessaggioInboxIncludi l'originale nella rispostaIncludi il testo del messaggio originale quando rispondi (non raccomandato perchУЈ occupa banda passante!!!)In ArrivoAccrediti in ArrivoColore in ArrivoFiles in ArrivoMessaggi in ArrivoIncrementa dimensioneDati GPS non correttiCarattere non valido nel nominativoInvalid port stringValore non validoValore non valido perIscrizione al canaleLinguaGrandeLatitudineTempo di attesa per spedire un messaggio di warmup per evitare l'auto-spengimento dei palmariAccoda righe del registroCaricatoCaricamentoLocalePorta LocaleTrova un indirizzoTrovata PosizioneLogRegistra traffico chatLongitudineRicercaRicerca per indirizzoQRAPrincipaleInvia il messaggio selezionato manualmente ad un altra stazioneMappaPercorso Salvataggio MappeSegna lettoSegna da leggereSegna il messaggio selezionato come lettoSegna il messaggio selezionato come da leggereMaxMessaggioMessaggio InviatoModelli di MessaggioAttivitУ  MessaggiTrasferimento messaggio di %s avviato con %sMessaggiMetricoMinMiscAncoraPacchetti a dimensione fissa multipliIl proprio SSID WinlinkNomeNegoziazione effettuataReteCreaNuovo Messaggio ChatNuovo Segnaposto quiBlocco di avvio non ricevutoSupporto per ridimensionamento immagine mancante. Invio inalterato?NienteColore AvvisoRegole per Avviso (RegEx)AdessoNumero di pacchetti DisattivoIn UNIX, qualcosa come '/dev/ttyUSB0'In Windows, qualcosa tipo 'COM12'Connesso IncustoditoConnesso (D-RATS)Apri Chat privataOpzioniOutboxIn uscitaColore in uscitaEmail in uscitaCopertura (overlay)Server POP3Dimensione pacchettiPacchettiPasswordPassword per l'autenticazione SMTPPercorsiPersonalePingInvia ping a tuttiInvia PingRisposta al pingPingBlocco collegamentoIntervallo di InterrogazionePortaPorta e ModoPorta:Rapporto di PosizionePreferenzeAnteprimaStampabileStampabile (area visibile)Limite messaggio QSTQSTQualitУ Interroga UtenteIntervallo di cancellazione della codaMessaggi RapidiRSSRadioTesto grezzoDavvero vuoi eliminare %s?Davvero vuoi cancellare?RicevutoEventi RicevutiRicezioneFile in ricezioneRecipienteAggiornareRemotoPorta RemotaTrasferimenti file remotiStazione remota:EliminaCancella FiltroRinominaRispondiRispondi al messaggio selezionatoRichiedi PosizioneRichiedi tutte le posizioniRichiesto controllo postaRichiesto file %sRichiesta lista fileRichiesto messaggio %sRichiesta lista messaggiRichiede il riavvio del D-RATSReimpostaRidimensionaRiavviaRecupero aRecupero in corsoInformazioni di RoutingLancia la provaLanciatoGateway SMTPPassword SMTPServer SMTPNome Utente SMTPSalvaSalva ImmagineSalva Immagine (area visibile)Righe da scorrere indietroSecondi fra ogni tentativo di processare messaggi inoltrati. Non impostare un valore troppo basso!InviaComandi D*QueryInvia FileSpedisci i messaggi di Posta in UscitaSpedizione viaSpedito/RicevutoMittenteSentSerialeSeriale Network TNC Dongle AGWPEPorta SerialePorta seriale per GPS esterno MNEA compatibileServerImpostazioniMostraMostra registro di debugMostra il tipo evento:Mostra elenco stazioniMostra aggiornamenti di stato in chatMostra ora in UTCPannello LateraleMessaggio di chiusuraMessaggio di aperturaQuesto УЈ il primo avvio del D-RATS,Siti (separati da virgola)DimensioneDimensione (bytes)PiccoloAnnusare (sniff)Sessione socket %s avviata con %sSuoniIndirizzo d'AvvioStazione di origineIndicare file .WAV da usareAvviaDimensione inizialePosizione StaticaStazioneStazione TTLMappa della StazioneLa stazione deve essere Stazione:StazioniStatisticheStatoStopOggettoSimbolo:Inoltro TCPGateway TCPTNCPorta TNCTabella:ProvaProva ConnettivitУ Prova parametriTipo provaTestoStringa di riposta alle richieste di ping.Lo standard NMEA УЈ 4800Il carattere simbolo per i beacon GPS-ALa tavella dei caratteri simbolo per i beacon GPS-ANon ci sono parametri per operazioni via DongleQuesto УЈ il messaggio che le altre stazioniQuesto УЈ lo stato che le altre stazioniQuesta funzione permette di inviare comandi D*Query al gateway locale (se supportato). Digita un comando qui (come wx) e clicka Invia. Se il gateway УЈ in funzione il risultato apparirУ  nella finestra Chat.OraOrario delle PosizioniTimbra con data e ore i messaggi di chatBottoni della barra delle opzioniTotaleModalitУ  InseguimentoTrasferimento CompletatoTrasferimento InterrottoTrasferimentiTrashTratta il testo grezzo (e i rifiuti) come dati di chat e li visualizza sul monitorTipoSCONOSCIUTOImpossibile connettersiImpossibile aprire la porta GPSImpossibile aprire il file %s: %sImpossible aprire: messaggio in usoImpossibile analizzare l'indirizzoImpossibile spedire: messaggio in usoUnitУ SconosciutoTipo immagine sconosciutaCarico (upload)Usa GPS esternoUsa SSLNome UtenteUtente per l'autenticazione SMTP. Disabilitato se vuoto.Info Sistema Operativo e VersioneConnessione WL2KWL2K Network ServerStazione RMS WL2KIn attesa primo bloccoIn attesa di rispostaIn attesa avvio trasferimentoLunghezza warm-upIntervallo di warm-upMeteo (WU)Quando abilitato, i campi 'orario' dei modelli sono inizializzati all'ora corrente in UTC. Quando disabilitato, predefinito all'ora localeDigitare un nominativo per continuareDigitare un nominativo di stazioneAltitudine attualeLa tua latitudine corrente. Usa gradi decimali (DD.DDDD) oppure D*M'". Usa uno spazio per passare da gradi a minuti e secondi.La tua longitudine corrente. Usa gradi decimali (DD.DDDD) oppure D*M'". Usa uno spazio per passare da gradi a minuti e secondi.Zoom_Modifica_File_Aiuto_Visualizzaa b a b ca b c viabyteschatgradiinesistente. Vuoi crearlo?inoltro adagtk-aboutgtk-addgtk-cancelgtk-cleargtk-closegtk-deletegtk-editgtk-okgtk-preferencesgtk-removeoreadesso УЈultima posizione adella dimensionesulla portapagina 1pagina 2pagina 3pagina 4pagina 5ricevutoricevuto darelazioneRichiesto controllo postariprovaha dettosecondiinviad-rats-0.3.3/locale/nl/000077500000000000000000000000001160617671700145735ustar00rootroot00000000000000d-rats-0.3.3/locale/nl/LC_MESSAGES/000077500000000000000000000000001160617671700163605ustar00rootroot00000000000000d-rats-0.3.3/locale/nl/LC_MESSAGES/D-RATS.mo000066400000000000000000000115571160617671700176600ustar00rootroot00000000000000о•fL‰| ЁЈЏГЛ#Пу ь ї     - 7 > T b i n { ˆ   Ж Л П Ь у ш ѕ ў  " + 4 B H S d n € … ‰ ‘ – ž Ђ Ї Д Ф Э м х ы ј   ! & 4 @ O U \ h ~ … ‹ œ Ж М Ъ ж ф ѕ њ    - > N ] e n u } Œ ˜ Ÿ Љ Џ З Ш а й ч і  w Š’ ˜ЂЈ4­т щ ѓ%9 IS m {… Œ š Ї ДР кф ші  '7M R_u |‰ Ÿ ЋЙПХЭвкр х№  $ * 7E Vd j xƒ’ ˜ЅЖ Ыз рю  (<PWfm|  › ЈГЛФЫг тю ѕ   , ; HSfPf-a9G@e!J#LTH&.[W0?bc B );$]748FO=1Q*Z,U(E'6dMK5+" V\Y >2S/<`X:N_ I%RDA^C 3AccessActionAddAddressAllAllow remote stations to pull formsAltitudeAppearanceBlock sizeBothCallsignChatChat fontChoose a formClear AllConfigConnected to InternetCreate filterDeleteEditEmail AccessEmail FilterEnabledExternal GPSForce transmission delayFormGPSGPS-A SymbolGoing offline (D-RATS)HostIgnore RegExIncomingIncoming AccountsInvalid value forLanguageLatitudeLoad log tailLocalLocal PortLog chat trafficLongitudeLookup by addressMainMapMessageNameNetworkNewNoneNotice RegExOnline (D-RATS)OutgoingOutgoing EmailPasswordPathsPing StationPing replyPipeline blocksPoll IntervalPortPort and ModePreferencesQuick MessagesRadioRemoteRemote PortRemote file transfersRemoveReplyRequest PositionRequires a D-RATS restartResetSMTP PasswordSMTP ServerSMTP UsernameScrollback LinesSendSerial PortServerShow debug logShow time in UTCSign-off MessageSign-on MessageSource AddressStationStationsStatusSymbol:TCP ForwardingTCP GatewayTable:TransfersUnitsUnknownUse External GPSUse SSLUsernameWarmup LengthWarmup timeoutYour current altitude_FileProject-Id-Version: DRats Report-Msgid-Bugs-To: POT-Creation-Date: 2011-01-01 14:08-0800 PO-Revision-Date: 2009-03-18 01:40-0800 Last-Translator: N1KEZ Language-Team: WashCoARES - N1KEZ Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Poedit-Language: Dutch X-Poedit-Country: NETHERLANDS ToegangAktieToevoegenStuurAlleSta stations op afstand toe formlieren te downloadenHoogteVertoningBlock grootteBeideCallsignChatChat lettertypeKies een forumulierVerwijder allesInstellenVerbindt met het InternetCreeer filterVerwijderBewerkEmail ToegangEmail filterIngeschakeldExterne GPSForceer verzendvertragingFormulierGPSGPS-A SymboolGaat offline (D-RATS)HostNegeer RegExInkomendInkomende namenOngeldige waarde voorTaalBreedtegraadLaad einde logbestandLokaalLokale poortSla chat berichten opLengtegraadZoek op adresStartKaartBerichtNaamNetwerkNieuwGeenMeld RegExOnline (D-RATS)UitgaandUitgaande EmailWachtwoordPadenPing StationPing antwoordPipeline blokkenTest intervalPoortPoort en ModeVoorkeurenSnel BerichtenRadioAndere zijdePoort op afstandBestandsverzendingenVerwijderenAntwoordVraag positieD-RATS start opnieuw opResetSMTP WachtwoordSMTP ServerSMTP GebruikersnaamTerug blader regelsZendenSerieele poortServerToon debug logToon tijd in UTCUitlogberichtInlogberichtBron adresStationStationsStatusSymboolTCP DoorsturenTCP GatewayTabel:VerzendingenEenhedenOnbekendGebruik GPSGebruik SSLGebruikersnaamOpwarmlengteOpwarmtijdDe huidige hoogte _Bestandd-rats-0.3.3/setup.cfg000066400000000000000000000001001160617671700145330ustar00rootroot00000000000000[bdist_rpm] requires = libxml2-python, libxslt-python, pyserial d-rats-0.3.3/setup.py000066400000000000000000000071751160617671700144470ustar00rootroot00000000000000import sys from d_rats.version import DRATS_VERSION import os def win32_build(): from distutils.core import setup import py2exe try: # if this doesn't work, try import modulefinder import py2exe.mf as modulefinder import win32com for p in win32com.__path__[1:]: modulefinder.AddPackagePath("win32com", p) for extra in ["win32com.shell"]: #,"win32com.mapi" __import__(extra) m = sys.modules[extra] for p in m.__path__[1:]: modulefinder.AddPackagePath(extra, p) except ImportError: # no build path setup, no worries. pass opts = { "py2exe" : { "includes" : "pango,atk,gobject,cairo,pangocairo,win32gui,win32com,win32com.shell,email.iterators,email.generator", "compressed" : 1, "optimize" : 2, "bundle_files" : 3, # "packages" : "" } } setup( windows=[{'script' : "d-rats", 'icon_resources': [(0x0004, 'd-rats2.ico')]}, {'script' : 'd-rats_repeater'}, {'script' : 'd-rats_mapdownloader'}], data_files=["C:\\GTK\\bin\\jpeg62.dll"], options=opts) def macos_build(): from setuptools import setup import shutil APP = ['d-rats-%s.py' % DRATS_VERSION] shutil.copy("d-rats", APP[0]) DATA_FILES = [('../Frameworks', ['/opt/local/lib/libpangox-1.0.0.2203.1.dylib']), ('../Resources/pango/1.6.0/modules', ['/opt/local/lib/pango/1.6.0/modules/pango-basic-atsui.so']), ('../Resources', ['images', 'ui']), ] OPTIONS = {'argv_emulation': True, "includes" : "gtk,atk,pangocairo,cairo"} setup( app=APP, data_files=DATA_FILES, options={'py2app': OPTIONS}, setup_requires=['py2app'], ) def default_build(): from distutils.core import setup from glob import glob desktop_files = glob("share/*.desktop") form_files = glob("forms/*.x?l") image_files = glob("images/*") image_files.append("d-rats2.ico") image_files.append("share/d-rats2.xpm") ui_files = glob("ui/*") _locale_files = glob("locale/*/LC_MESSAGES/D-RATS.mo") _man_files = glob("share/*.1") man_files = [] for f in _man_files: os.system("gzip -c %s > %s" % (f, f+".gz")) man_files.append(f+".gz") locale_files = [] for f in _locale_files: locale_files.append(("/usr/share/d-rats/%s" % os.path.dirname(f), [f])) print "LOC: %s" % str(ui_files) setup( name="d-rats", description="D-RATS", long_description="A communications tool for D-STAR", author="Dan Smith, KK7DS", author_email="kk7ds@danplanet.com", packages=["d_rats", "d_rats.geopy", "d_rats.ui", "d_rats.sessions"], version=DRATS_VERSION, scripts=["d-rats", "d-rats_mapdownloader", "d-rats_repeater"], data_files=[('/usr/share/applications', desktop_files), ('/usr/share/icons', ["share/d-rats2.xpm"]), ('/usr/share/d-rats/forms', form_files), ('/usr/share/d-rats/images', image_files), ('/usr/share/d-rats/ui', ui_files), ('/usr/share/d-rats/libexec', ["libexec/lzhuf"]), ('/usr/share/man/man1', man_files), ('/usr/share/doc/d-rats', ['COPYING']), ] + locale_files) if sys.platform == "darwin": macos_build() elif sys.platform == "win32": win32_build() else: default_build() d-rats-0.3.3/share/000077500000000000000000000000001160617671700140255ustar00rootroot00000000000000d-rats-0.3.3/share/d-rats.1.gz000066400000000000000000000015421160617671700157220ustar00rootroot00000000000000‹'ШMd-rats.1TQoл6~зЏ8фeі*iёP Cв pmЇqи‚х (ъЃЄ“Х„"’ŠЋП#;Nж‡А%ѓюО;~їнЅЛ јЉЯ-1,юІГќ ’_ZеЕ{‹R‚ИскXш˜f-Zд1ЌІw‹LЃzQAР„€’uЦ{чX*YЛч‹йvЙ^GL’?bhй@Џ‡пРє…Свr%=‚В ъLћъ€еDŠ”Ѓуи?'у(ноТ<йLЗ9LртK/˜МсїЫЩх…GЬ2CеCO7Б 7P1KЙ”јDЩ4>qУхžlНc{L]hИ’j<%-+Е21дЪХдЈQ–xхRйщЌИa…@h†ŽАгХвf8Каё=X"xЌ-Иzy=œlEА=ƒUP[С—ыšяKUъ=—цЙЌњmY5‚юЬ5Sв+kЁVТCmL…FіLх_gђнАэьр]C Ž­gRгaЩk^žЈ|щg”цЗ^[Q•hf ь˜BЉкЖ—Мє$б•№xѓ$пN7>$џКZgљ2вO"ЃtГ„o :cрŸˆ.т]ч‹|ЖYfNŽбжЩ€2їL€+ *Uі-JJ\hŽ5Љˆдpъ*!’IYэъOсxWg@@ьuЁцmЅ@ђu—ЧЪIСhў7ДЊТq-iИДzтšЋˆ˜Мы…хIOtBй0ыF‹\pЫб\;‡N}ВšIC 4ю$ЗК/mЏшои)ъЧˆJhЭјкЩУyfЪ№РЄfхЃ“МгMЫКŽоSOвкѓ“‚ЈВ= "нФ  ёвЧйче= c #B1A48C", ", c #DDDDDC", "' c #937F62", ") c #6F4200", "! c #6A4817", "~ c #9C8F7C", "{ c #D5D5D5", "] c #D2D2D2", "^ c #D4D4D4", "/ c #D0D0D0", "( c #DCDCDC", "_ c #FBFBFB", ": c #ADA28E", "< c #8C692C", "[ c #9C742C", "} c #8D6828", "| c #96878B", "1 c #7F6D52", "2 c #AA9F90", "3 c #F7F7F7", "4 c #EFEFEF", "5 c #C7C7C7", "6 c #94815E", "7 c #9B732C", "8 c #815535", "9 c #AB858F", "0 c #DDDDDD", "a c #BDBDBD", "b c #F8F8F8", "c c #9C9489", "d c #A09381", "e c #90784D", "f c #98712B", "g c #805730", "h c #9C5161", "i c #B2A1A5", "j c #E2E2E2", "k c #E1E1E1", "l c #CFCFCF", "m c #C9C9C9", "n c #D0CECA", "o c #5E3902", "p c #6C4D19", "q c #744F2C", "r c #885A37", "s c #885342", "t c #8E4C56", "u c #9E5366", "v c #AB8F96", "w c #CDCDCD", "x c #75716B", "y c #322109", "z c #EBEBEB", "A c #725428", "B c #533009", "C c #6F3B36", "D c #7F4342", "E c #753F2D", "F c #67461B", "G c #A89E8E", "H c #F6F6F6", "I c #A5A19C", "J c #765B33", "K c #6C4000", "L c #684615", "M c #ADADAD", "N c #0E0E0E", "O c #616161", "P c #FCFCFC", "Q c #775A30", "R c #6C4B1B", "S c #BEB7AD", "T c #BCA5A5", "U c #905F4E", "V c #693E02", "W c #674412", "X c #5F5F5F", "Y c #060606", "Z c #1E1E1E", "` c #F2F2F2", " . c #64400C", ".. c #5F3800", "+. c #613A00", "@. c #6A3F00", "#. c #877458", "$. c #E6E5E5", "%. c #B88C8B", "&. c #E49897", "*. c #E59998", "=. c #A97065", "-. c #693F02", ";. c #D5D4D1", ">. c #606060", ",. c #AAAAAA", "'. c #CAC7C2", "). c #593500", "!. c #462900", "~. c #633B00", "{. c #745931", "]. c #E9E9E8", "^. c #DFDBDB", "/. c #BF8887", "(. c #976353", "_. c #6E4100", ":. c #9C8D78", "<. c #938167", "[. c #5B3600", "}. c #6E4F21", "|. c #EBEAEA", "1. c #D6D6D6", "2. c #C68C8B", "3. c #71471F", "4. c #68430D", "5. c #DFDEDB", "6. c #E0DFDD", "7. c #664007", "8. c #5A3600", "9. c #785D36", "0. c #F4F4F4", "a. c #E6E6E6", "b. c #DAD8D8", "c. c #C38A89", "d. c #D28C8B", "e. c #BF7F7F", "f. c #BD7E7D", "g. c #C28281", "h. c #BB7D7C", "i. c #805446", "j. c #623A00", "k. c #735426", "l. c #DEDCDA", "m. c #DDDCD9", "n. c #705225", "o. c #553300", "p. c #918068", "q. c #F9F9F9", "r. c #D9D9D9", "s. c #D8D8D8", "t. c #AE8180", "u. c #CA8786", "v. c #D08B8A", "w. c #AE736B", "x. c #663D00", "y. c #543508", "z. c #96856B", "A. c #C8C5BF", "B. c #E8E8E8", "C. c #E7E7E7", "D. c #98876D", "E. c #684108", "F. c #583400", "G. c #693F00", "H. c #6A4002", "I. c #CCC8C3", "J. c #CCCCCC", "K. c #DBDBDB", "L. c #A48B8A", "M. c #D7908F", "N. c #C18180", "O. c #AE7366", "P. c #5C3700", "Q. c #653C00", "R. c #673D00", "S. c #806846", "T. c #BBB7B7", "U. c #D38E8D", "V. c #996665", "W. c #604040", "X. c #704B4A", "Y. c #CE8989", "Z. c #BE7F7E", "`. c #B67979", " + c #E29796", ".+ c #9A644F", "++ c #6B3F00", "@+ c #663D01", "#+ c #E5E5E5", "$+ c #F3F3F3", "%+ c #CECECE", "&+ c #DAD3D3", "*+ c #BF9493", "=+ c #D38D8C", "-+ c #DA9291", ";+ c #A8706F", ">+ c #8E5F5E", ",+ c #A06B6A", "'+ c #E09695", ")+ c #BC7E7D", "!+ c #654022", "~+ c #623D06", "{+ c #674209", "]+ c #633D03", "^+ c #BBB4AA", "/+ c #E1DFDF", "(+ c #C4AEAE", "_+ c #BF8F8F", ":+ c #B47878", "<+ c #CF8A89", "[+ c #653D02", "}+ c #603900", "|+ c #683E01", "1+ c #694004", "2+ c #69460F", "3+ c #876425", "4+ c #9A732C", "5+ c #79571E", "6+ c #643E05", "7+ c #AA9D8A", "8+ c #CDC1C1", "9+ c #C8B0B0", "0+ c #B79F9F", "a+ c #E1BBBA", "b+ c #C6A79E", "c+ c #AC936D", "d+ c #AB926C", "e+ c #A28C6D", "f+ c #BAA98E", "g+ c #C6B499", "h+ c #CBBBA3", "i+ c #D8CFC1", "j+ c #DFDFDD", "k+ c #694D38", "l+ c #62361C", "m+ c #5B3601", "n+ c #6E4E17", "o+ c #8E6927", "p+ c #633E06", "q+ c #A79A86", "r+ c #AE6F7F", "s+ c #773C4B", "t+ c #7F4652", "u+ c #895A47", "v+ c #663E0D", "w+ c #65430E", "x+ c #896525", "y+ c #B0AAA0", "z+ c #D6BFC5", "A+ c #7B4F5A", "B+ c #D89190", "C+ c #AE746F", "D+ c #693E00", "E+ c #846124", "F+ c #966F2A", "G+ c #D9D8D6", "H+ c #F6DDDD", "I+ c #E8A4A3", "J+ c #B97B7B", "K+ c #644008", "L+ c #8E7342", "M+ c #FEFBFB", "N+ c #EFDEDE", "O+ c #B49F80", "P+ c #5E3610", "Q+ c #5E3801", "R+ c #623A01", "S+ c #6E4B11", "T+ c #B1A590", "U+ c #AF9899", "V+ c #914A5C", "W+ c #7E434E", "X+ c #936354", "Y+ c #825D37", "Z+ c #8B6A2E", "`+ c #F2EDEE", " @ c #A67E88", ".@ c #AE596F", "+@ c #A76C6E", "@@ c #DF9594", "#@ c #835D37", "$@ c #A99D88", "%@ c #E8D6DA", "&@ c #C99897", "*@ c #C28280", "=@ c #8E6928", "-@ c #8A7348", ";@ c #F9F7F7", ">@ c #D9B6B6", ",@ c #E19E9D", "'@ c #B2837A", ")@ c #815A2E", "!@ c #8E4F5D", "~@ c #D9D4D5", "{@ c #F4F2F2", "]@ c #D8C9AC", "^@ c #A37E3B", "/@ c #936E2B", "(@ c #9B8B6D", "_@ c #936A74", ":@ c #AB576D", "<@ c #8A5361", "[@ c #EAE8E9", "}@ c #ECE8E0", "|@ c #D5D3CF", "1@ c #FDFDFD", "2@ c #A49196", "3@ c #924A5D", "4@ c #A07983", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . + @ # . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . $ % & * . . . . . . . . . . . = - ; > , . . . . . . . . ", ". . . . . . . . . . . . . . . . . ' ) ! ~ { ] ^ / ( . . . . _ : < [ [ } | . . . . . . . . ", ". . . . . . . . . . . . . . . . . 1 2 3 . . . . . 4 5 . . # 6 7 [ [ [ 8 9 . . . . . . . . ", ". . . . . . . . . . . . . 0 / ^ a { . . . . . . . . b c d e [ [ [ f g h i . . . . . . . . ", ". . . . . . . . . . . . j k 4 l m . . . . . . . . . . n o p q r s t u v _ . . . . . . . . ", ". . . . . . . . . . . . w + x y z . . . . . . _ . . . . A % B C D E F G H . . . . . . . . ", ". . . . . . . . . . . . I J K L . . . . . . M N O P . . Q ) ) ) ) ) ) ) R S . . . . . . . ", ". . . . . . . . . . b T U V ) W + . . . . . X Y Z ` . P .) ..+.) ) ) ) ) @.#.` . . . . . ", ". . . . . . . . . $.%.&.*.=.-.@.;.. . . . . ] >.,.. . '.% ).!.~.) ) ) ) ) ) ) {.].. . . . ", ". . . . . . . . ^./.*.*.*.*.(._.:.. . . . . . . . . . <.) [.K ) ) ) ) ) ) ) ) ) }.|.. . . ", "1.{ z . . . . ^.2.*.*.*.*.*.&.3.4.5.. . . . . . . . 6.7.% 8.) ) ) ) ) ) ) ) ) ) ) 9.0.. . ", ". . a.] ] ] b.c.*.d.e.f.g.f.h.i.j.k.l.. . . . . . m.n.) o.) ) ) ) ) ) ) ) ) ) ) ) ) p.. . ", ". q.r.s.0.3 t.*.u.v.*.*.*.*.*.w.x.).y.z.A.B.C.'.D.E.) F.G.) ) ) ) ) ) ) ) ) ) ) ) ) H.I.. ", "J.1.b _ K.L.*.*.*.M.N.&.*.*.*.O.) ) K P.P.Q.R._.) % F.@.) ) ) ) ) ) ) ) ) ) ) ) ) ) ) S.. ", ". . . K.T.U.*.V.W.X.Y.Z.`.f. +.+) ) ) ) ++Q.) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) @+#+", "$+%+J.$+&+*+=+-+;+>+,+&.*.'+)+!+K ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) R.~+{+]+% ) ) ^+", "1.. . . . . /+(+_+=+ +:+<+*.d.[+[.}+) ) ) ) |+1+) ) ) ) ) ) ) ) ) ) ) ++2+3+[ [ 4+5+6+) 7+", ". . . . . . . . . a.8+9+0+a+b+c+d+e+f+g+h+i+j+k+l+m+) ) ) ) ) ) ) ) K n+[ [ [ [ [ [ o+p+q+", ". . . . . . . . . . . . . . . . . . . . . . . r+s+t+u+v+) ) ) ) ) ) w+[ [ [ [ [ [ [ [ x+y+", ". . . . . . . . . . . . . . . . . . . . . . . z+A+B+*.C+K ) ) ) ) D+E+[ [ [ [ [ [ [ [ F+G+", ". . . . . . . . . . . . . . . . . . . . . . . . + H+I+J+R.) ) ) ) K+[ [ [ [ [ [ [ [ [ L++ ", "+ . . . . . . . . . . . . . . . . . . . . . . . . . M+N+O+|+P+Q+R+S+[ [ [ [ [ [ [ [ [ T+. ", ". . . . . . . . . . . . + . . . . . . . . . . . . . . . . U+V+V+W+X+Y+[ [ [ [ [ [ [ Z+|.. ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . `+ @.@+@*.@@#@[ [ [ [ [ [ $@. . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . %@&@*.*.*@=@[ [ [ [ -@3 . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ;@>@,@'@f [ [ [ )@!@~@. . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . {@+ ]@^@/@(@_@:@<@[@. ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }@|@. 1@2@3@4@. ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ", ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "}; d-rats-0.3.3/share/d-rats_mapdownloader.1.gz000066400000000000000000000015421160617671700206360ustar00rootroot00000000000000‹'ШMd-rats_mapdownloader.1T]oу6|зЏXфщвкъЙшЁEP№Х.’Ђ‰ШEQ Р—/Љ’T ѕзwHйI.M‹њХ’8ћ53ЫђўŒўзяŠЧ­o–—еЭПš“ѕNЉєT”HёГі!R/Мш8ВŸбэђf=ЃаКСHЊ™„1дˆ>dtХГђ%МZ_юЎ7З/#ѓfд‰‡o( uр&jgs[іЯ  ŸkИЫ ЬˆДяО?ŸхџХyQюЎh5П[юЊO7ЫэjѓћэЏ›хj}G :ћe0#->Ьшлї‹їg9џжАШ(?˜+Ж:ДlљЅ=?ъ эgЙV/і\Іаi@з1e‚:бxfЄ\ŠQьй6|‘AЅmOфJDm˜кБGё4fйŽ'>П‰’Ь„0Ќ"Ѕ~ЕŸЮъщьј™ЂЃмMX МоЗ]њНЖси–zн–вЦ`жщXщW-}qZћг)ђБ„#ІкГx˜ЁЇэO/іыqзЧ1CЇ[GRчЁчF+нч\КxxCџыsЭгŒV7‹eyAљЗ9ikк6мeR\Iы< ТŠž=лн.nV3rUC“PŠj1Иш]rmtsъ^Ў–ЛѕнэiФ<џyF͘p{јžмX9ЎН4:f0Оcћ%#ac sрц‚3"ѕйOчГxŸgХюš.ѓэbW~иЎ6ЋХnЕЅ9Нљ}TЭœбoчoпФдХТ!YѓqФHО“ŽсбDЧš?ЁЊхOвIН‡-–Фž‹šf3=SФІЕ5nF­ 1-[ж5_DЇBwOИ6в‰J1uг€тyТЂ›ž\№њUбJŠ[OЁ_йNЯЖ*йŽЏЩЊ[ђ€пЪ}чбЅнKэŽmЕ/лjЅR˜5™[љЂЅЏЌ•}В"[O01U–Хcrp§Ђ;qапЭ‰ћСOб5ѕа:‚šЛkйЪњЪ/ФfEyE–5Йо}А<0hВєг"0&ˆ?за{І= 1ХмI1МќуіnSЎё№Ž^dЩŠэšў$3Д§•aКsЙ*—лѕ&ˆ5лm Q( §RcъБgэІ–мBZШkйkг#Ўj6›ьЁ}їТўаnЉ…&1 Jж‘s”ЈQЅЁƒЧ9ЌЛЄ|є2*/№r™—ЛХ–ЌhЄqtйjB†FЖQ†>k-џ3B’vФwТЧ ТZjlj@-Aоћ…З§0Ц5ѕ&“wG\S`РіБЩ‚жШЄœyj а‡U1mvtіVДв GІ“laщ§бщі*ъ3к4ћl„X\A76ь…TмœоhН•ћбІ§8ЕжРЅZ3Т ЯaЎc3ЎШIжмоEZЫФы`ЭЇ Тв !jtъїЗїt$0IмMк‹ЯГФ‹2и‡'н8/lœ?šќС@˜ЎcPђwžs^d lШd' єЌ6T—КVc1AuДЗл=фн ?ИАВВ3‡WтOМkZон^­пЇ бЏщUv9 ЇлW№A(20v:ЩеЄ$ Wу>•ŽЗдГsЅ а:п˜бŸ-Ÿ+;Ѓ8лŽЃбё‘zг0Тд”№_мяЎяЖЧ…ІƒРзJяЁˆjЂKАXі€1-Эvя…{щ!zZ§ŠfYиќШ;W‰@юG HgCќЧ}@eMˆŒ-мќ K@„€d-rats-0.3.3/share/d-rats_repeater.desktop000066400000000000000000000004021160617671700204750ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Encoding=UTF-8 Type=Application Exec=d-rats_repeater Icon=d-rats2 StartupNotify=true Terminal=false Categories=Application;HamRadio MimeType=inode/directory Name=D-RATS Repeater Comment=D-RATS Repeater GenericName=D-RATS Repeater d-rats-0.3.3/ui/000077500000000000000000000000001160617671700133405ustar00rootroot00000000000000d-rats-0.3.3/ui/addport.glade000066400000000000000000001066711160617671700160060ustar00rootroot00000000000000 5 Add a port center-on-parent dialog False True 2 True True 2 2 True 0 Serial Network TNC Dongle AGWPE 1 2 1 2 GTK_FILL True True 1 2 GTK_FILL True Type 1 2 GTK_FILL True Name GTK_FILL False 0 True 2 4 True 1 True 0.5 True 4 4 4 4 True True False True 2 2 4 4 True 3 1200 2400 4800 9600 19200 38400 57600 115200 1 2 1 2 GTK_FILL True Baud Rate 1 2 GTK_FILL True a b 1 2 GTK_FILL True Serial Port GTK_FILL True page 1 False tab True 3 2 4 4 True True 1 2 2 3 GTK_FILL True True 9000 1 32768 1 10 10 1 2 1 2 GTK_FILL True True 1 2 GTK_FILL True Password 2 3 GTK_FILL True Port 1 2 GTK_FILL True Host Address GTK_FILL 1 True page 2 1 False tab True 5 2 4 4 True The baud rate used to communicate with the TNC (not the rate over the air) 3 1200 2400 4800 9600 19200 38400 57600 115200 1 2 1 2 GTK_FILL True Baud Rate 1 2 GTK_FILL True True If the TNC is a multi-port unit, this specifies which port will be used (starting with 0 as the first one) 0 0 15 1 10 10 1 2 2 3 GTK_FILL True TNC Port 2 3 GTK_FILL True a b 1 2 GTK_FILL True Serial Port GTK_FILL Enabled True True False When enabled, D-RATS will encapsulate its packets in AX.25 UI frames which can be passed through a digipeater True 1 2 3 4 GTK_FILL True AX.25 3 4 GTK_FILL True Digi Path 4 5 True False True AX.25 Digi path (CALL1,CALL2,...) 1 2 4 5 2 True page 3 2 False tab True There are no parameters for dongle operation 3 True page 4 3 False tab True 6 2 True Address True Port 1 2 True True 1 2 GTK_FILL True True 100 -2 65536 1 10 10 1 2 1 2 True Username 2 3 True False True 1 2 2 3 True Password 3 4 True False True False 1 2 3 4 True Digi Path 4 5 True False True 1 2 4 5 4 False True page 5 4 False tab True <b>Parameters</b> True center label_item 2 1 True end gtk-add 1 True True True True False False 0 gtk-cancel True True True True False False 1 False end 0 d-rats-0.3.3/ui/addport.glade.h000066400000000000000000000026001160617671700162170ustar00rootroot00000000000000char *s = N_("1200\n" "2400\n" "4800\n" "9600\n" "19200\n" "38400\n" "57600\n" "115200"); char *s = N_("Parameters"); char *s = N_("AX.25"); char *s = N_("AX.25 Digi path (CALL1,CALL2,...)"); char *s = N_("Add a port"); char *s = N_("Address"); char *s = N_("Baud Rate"); char *s = N_("Digi Path"); char *s = N_("Enabled"); char *s = N_("Host Address"); char *s = N_("If the TNC is a multi-port unit, this\n" "specifies which port will be used\n" "(starting with 0 as the first one)"); char *s = N_("Name"); char *s = N_("Password"); char *s = N_("Port"); char *s = N_("Serial\n" "Network\n" "TNC\n" "Dongle\n" "AGWPE"); char *s = N_("Serial Port"); char *s = N_("TNC Port"); char *s = N_("The baud rate used to communicate with\n" "the TNC (not the rate over the air)"); char *s = N_("There are no parameters for dongle operation"); char *s = N_("Type"); char *s = N_("Username"); char *s = N_("When enabled, D-RATS will encapsulate\n" "its packets in AX.25 UI frames which can be\n" "passed through a digipeater"); char *s = N_("a\n" "b\n" ""); char *s = N_("page 1"); char *s = N_("page 2"); char *s = N_("page 3"); char *s = N_("page 4"); char *s = N_("page 5"); d-rats-0.3.3/ui/addport.glade.orig000066400000000000000000001110311160617671700167270ustar00rootroot00000000000000 5 Add a port center-on-parent dialog False True 2 True True 2 2 True 0 Serial Network TNC TNC/AX.25 Dongle AGWPE 1 2 1 2 GTK_FILL True True 1 2 GTK_FILL True Type 1 2 GTK_FILL True Name GTK_FILL False 0 True 2 4 True 1 True 0.5 True 4 4 4 4 True True False True 2 2 4 4 True 3 1200 2400 4800 9600 19200 38400 57600 115200 1 2 1 2 GTK_FILL True Baud Rate 1 2 GTK_FILL True a b 1 2 GTK_FILL True Serial Port GTK_FILL True page 1 False tab True 3 2 4 4 True True 1 2 2 3 GTK_FILL True True 9000 1 32768 1 10 10 1 2 1 2 GTK_FILL True True 1 2 GTK_FILL True Password 2 3 GTK_FILL True Port 1 2 GTK_FILL True Host Address GTK_FILL 1 True page 2 1 False tab True 3 2 4 4 True 3 1200 2400 4800 9600 19200 38400 57600 115200 1 2 1 2 GTK_FILL True Baud Rate 1 2 GTK_FILL True True 0 0 15 1 10 10 1 2 2 3 GTK_FILL True TNC Port 2 3 GTK_FILL True a b 1 2 GTK_FILL True Serial Port GTK_FILL 2 True 3 2 4 4 True 3 1200 2400 4800 9600 19200 38400 57600 115200 1 2 1 2 GTK_FILL True Baud Rate 1 2 GTK_FILL True True 0 0 15 1 10 10 1 2 2 3 GTK_FILL True TNC Port 2 3 GTK_FILL True a b 1 2 GTK_FILL True Serial Port GTK_FILL 2 True page 3 2 False tab True There are no parameters for dongle operation 3 True page 3 2 False tab True 6 2 True Address True Port 1 2 True True 1 2 GTK_FILL True True 100 -2 65536 1 10 10 1 2 1 2 True Username 2 3 True False True 1 2 2 3 True Password 3 4 True False True False 1 2 3 4 True Digi Path 4 5 True False True 1 2 4 5 4 False True page 5 4 False tab True <b>Parameters</b> True center label_item 2 1 True end gtk-add 1 True True True True False False 0 gtk-cancel True True True True False False 1 False end 0 d-rats-0.3.3/ui/mainwindow.glade000066400000000000000000002171741160617671700165260ustar00rootroot00000000000000 D-RATS 640 480 True vertical True True _File True True Tools True True Send D*Query True False True gtk-dialog-question Network Proxy True False True gtk-network Broadcast Text File True False Import Message True True False Export Message True True False Message Templates True True False Ping Station True True False gtk-preferences True True True True Connected to Internet True True gtk-quit True True True _Edit True gtk-cut True True True gtk-copy True True True gtk-paste True True True gtk-delete True True True True _View True True Show station list Side Pane True Add filter True False Delete Filter True False gtk-clear True True Log True False Map True True False True _Help True Show debug log True True False gtk-about True True True False False 0 True True 0 in True True True True True both False 0 True True 150 True True True automatic automatic in True True False True True True automatic automatic True True True True True 150 True Messages False tab True vertical True False 0 True True bottom True Main False tab 1 True True 0 A B C False False 0 True 0 in True True 1 Send 50 True True True False False end 2 False 2 True True True True True automatic automatic True True 0 True vertical 2 center gtk-add True True True True False False 0 gtk-remove True True True True False False 1 False 1 True Quick Messages label_item False 3 True True True True True automatic automatic 200 True True 0 True vertical 2 end gtk-add True True True True False False 0 gtk-edit True True True True False False 1 gtk-remove True True True True False False 2 False 1 True QSTs label_item False 4 1 True Chat 1 False tab True True 300 True True True both False True False 0 True Local center 1 False 0 True True automatic automatic True True 1 False True True True True both False True False 0 True 1 16 True gtk-missing-image False 2 False False 0 True True Station: False 4 0 True a b c 1 True a b c False 2 False 1 True True automatic automatic True True 2 False True 300 True Files 2 False tab True True True Show event type: False False 4 0 True All File Transfers Form Transfers Position Reports Pings False False 1 True Containing text: False False 4 2 True True True 3 False False 0 True True automatic automatic True True True 1 3 True Event Log 3 False tab label_item 2 0 True vertical True 0.5 none True True never automatic 100 True True False True 25 True <b>Stations</b> True label_item 0 True 0.5 none True vertical 15 True 0 0 15 True True 1 True <b>My Status</b> True label_item False False 1 False 1 1 True True 2 False 0 100 True 2 False 1 False False 2 5 Edit Map Source center-on-parent dialog False True 2 True True 2 True True 1 2 True Name False 0 1 True end gtk-close -5 True True True True False False 1 False end 0 350 350 5 Edit Map Sources center-on-parent dialog False True 2 True True True automatic automatic True True 0 True 4 gtk-add 75 True True True True False False 1 gtk-edit 75 True True True True False False 2 gtk-delete 75 True True True True False False 3 False False 1 1 True end gtk-close -5 True True True True False False 1 False False end 0 5 normal False True vertical 2 True vertical True 10 10 <big><b>D*Query Tool</b></big> True 0 True 10 11 This utility will allow you to send D*Query commands to the local gateway (if supported). Type a command here (such as <i>wx</i>) and hit <b>Send</b>. If successful, the result will appear in the chat window. True center True 1 True True 5 Command: False False 0 True True 1 2 1 True end gtk-ok -5 True True True True False False 0 gtk-cancel -6 True True True True False False 1 False end 0 d-rats-0.3.3/ui/mainwindow.glade.h000066400000000000000000000036651160617671700167520ustar00rootroot00000000000000char *s = N_("My Status"); char *s = N_("Stations"); char *s = N_("D*Query Tool"); char *s = N_("A\n" "B\n" "C"); char *s = N_("Add filter"); char *s = N_("All\n" "File Transfers\n" "Form Transfers\n" "Position Reports\n" "Pings"); char *s = N_("Broadcast Text File"); char *s = N_("Chat"); char *s = N_("Command:"); char *s = N_("Connected to Internet"); char *s = N_("Containing text:"); char *s = N_("D-RATS"); char *s = N_("Delete Filter"); char *s = N_("Edit Map Source"); char *s = N_("Edit Map Sources"); char *s = N_("Event Log"); char *s = N_("Export Message"); char *s = N_("Files"); char *s = N_("Import Message"); char *s = N_("Local"); char *s = N_("Log"); char *s = N_("Main"); char *s = N_("Map"); char *s = N_("Message Templates"); char *s = N_("Messages"); char *s = N_("Name"); char *s = N_("Online\n" "Unattended"); char *s = N_("Ping Station"); char *s = N_("QSTs"); char *s = N_("Quick Messages"); char *s = N_("Send"); char *s = N_("Send D*Query"); char *s = N_("Show debug log"); char *s = N_("Show event type:"); char *s = N_("Show station list"); char *s = N_("Side Pane"); char *s = N_("Station:"); char *s = N_("This utility will allow you to send D*Query commands to the local gateway (if supported). Type a command here (such as wx) and hit Send. If successful, the result will appear in the chat window."); char *s = N_("_Edit"); char *s = N_("_File"); char *s = N_("_Help"); char *s = N_("_View"); char *s = N_("a\n" "b\n" "c"); char *s = N_("a\n" "b\n" "c\n" ""); char *s = N_("gtk-about"); char *s = N_("gtk-add"); char *s = N_("gtk-cancel"); char *s = N_("gtk-clear"); char *s = N_("gtk-close"); char *s = N_("gtk-delete"); char *s = N_("gtk-edit"); char *s = N_("gtk-ok"); char *s = N_("gtk-preferences"); char *s = N_("gtk-remove"); d-rats-0.3.3/ui/mainwindow.gladep000066400000000000000000000004051160617671700166710ustar00rootroot00000000000000 FALSE