diff --git a/.gitignore b/.gitignore index b230d97..64e6b50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -.idea/* +.idea/ /logs levels.db* /build @@ -9,3 +9,5 @@ levels.db* *.spec *.log govno_rutony.py +/node_modules/ +/http \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3259173 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: generic +sudo: true + +addons: + apt: + packages: + - xvfb + +services: + - docker + +branches: + only: + - develop + +env: + - TEST_SUITE="runtime" + - TEST_SUITE="pylint" + - TEST_SUITE="jslint" + - TEST_SUITE="csslint" + +matrix: + fast_finish: true + allow_failures: + env: + - TEST_SUITE="pylint" + - TEST_SUITE="jslint" + - TEST_SUITE="csslint" + +script: + - ./travis/main.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fcc159b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM deforce/alpine-wxpython:latest + +MAINTAINER CzT/DeForce + +ADD . /usr/lib/python2.7/site-packages/LalkaChat + +RUN apk --update add --virtual build-deps build-base bzip2 git \ + libstdc++ openssl-dev py2-pip python2-dev tar wget wxgtk-dev xz && \ + + pip install requests cherrypy ws4py irc semantic_version jinja2 && \ + cd /usr/lib/python2.7/site-packages/LalkaChat && \ + mkdir -p ./conf && \ + mv ./docker/stubs/cefpython3 /usr/lib/python2.7/site-packages/ && \ + mv ./docker/run /usr/local/bin/ && \ + rm -rf ./docker && \ + python ./setup.py build && \ + + mv /usr/lib/python2.7/site-packages/ /usr/lib/python2.7/~site-packages/ && \ + apk del build-deps && \ + mv /usr/lib/python2.7/~site-packages/ /usr/lib/python2.7/site-packages/ && \ + rm -rf /var/cache/* /tmp/* /var/log/* ~/.cache && \ + mkdir -p /var/cache/apk + +VOLUME /usr/lib/python2.7/site-packages/LalkaChat + +CMD /usr/local/bin/run diff --git a/Dockerfile_test b/Dockerfile_test new file mode 100644 index 0000000..bebacc1 --- /dev/null +++ b/Dockerfile_test @@ -0,0 +1,13 @@ +FROM deforce/lalkachat-build-deps:latest + +MAINTAINER CzT/DeForce + +ADD . /usr/lib/python2.7/site-packages/LalkaChat + +RUN cd /usr/lib/python2.7/site-packages/LalkaChat && \ + mkdir -p ./conf && \ + mv ./docker/stubs/cefpython3 /usr/lib/python2.7/site-packages/ && \ + mv ./docker/run /usr/local/bin/ && \ + rm -rf ./docker && \ + python ./setup.py build +CMD /usr/local/bin/run diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e963df8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,622 @@ + 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 + diff --git a/README.md b/README.md index 6f7bc73..f078095 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,86 @@ -# LalkaChat +# LalkaChat [![Build Status](https://travis-ci.org/DeForce/LalkaChat.svg?branch=develop)](https://travis-ci.org/DeForce/LalkaChat) -Thanks to ftpud for fixing IE compatibility + LalkaChat is a program for displaying multiple chats from services is one place. + It's cross-platform and highly configurable. You can set the style of the chat, so it can look differently and you can tweak a lot of the settings. + Writen in modular way so you can easily add or remove any module that you need or do not need or write your own module if you lack available functionality. + Uses wxPython as a GUI interface and CEF as a browser window that renders current style. + HTTP Server backend is using CherryPy to provide ability for modules to use REST API calls to control chat behaviour. + Various modules that enrich the LalkaChat functionality. + Supports multiple languages being English and Russian at the moment, but core is already there + and you can translate to different languages if there is proper translations. + You can see how much viewers you have for each chat in the GUI (You can toggle this function on/off if you desire so). + +## Supported Websites + +* [Peka2tv](http://peka2.tv/) +* [GoodGame](https://goodgame.ru/) +* [Twitch.TV](https://www.twitch.tv/) + +## Architecture + +LalkaChat uses conveyor architecture delivering the messages. + +You configure the "Input" chat modules, such as Twitch/Goodgame and will start sending messages to the central queue + which will push the message to message process modules that are loaded by priority. +Those modules each process the message and pass the message to the next module. +Last one module is WebChat module which is HTTP backend that is responsible for hosting the style/theme and sending it to browser. + +## Available Modules for message processing + +* Blacklist - Allows user to blacklist\hide specific words or chatters. +* Cloud2Butt - Word substitution module. +* Dwarf Fortress - Special module that picks chatters by special keyword and writes them into file. +* Levels - Adds level icons to chatter, allows them to level up in the chat and be awesome by chatting. +* Logger - Logs all the messages that come via LalkaChat pipe. +* Mentions - Allows user to specify different words to be highlighted. + (Example: Your username differs in different chat and you want to highlight it from everywhere) +* Webchat - Core module to allow user to view the chat from browser/GUI + +## Information for Developers +TODO +### Messaging Module Basics +TODO // Code Examples +### Chat Module Basics +TODO // Code Examples +### GUI Module Basics +TODO // Code Examples + +## Installation from Source + + ToDo. Probably will be couple scripts that you can run from python. + +## Installation from Package (Windows) + +Unpack the zip to the folder you want, and run `LalkaChat.exe`. + +## Docker Availability +Docker build is available for testing LalkaChat, uses XPRA to display GUI from browser + +### Usage(GNU/Linux macOS Windows): + - [Install docker](https://docs.docker.com/engine/installation/) + - `docker run -d --name x11-bridge -e MODE="html" -p 10000:10000 -e XPRA_DISPLAY=:14 -e XPRA_PASSWORD= jare/x11-bridge` + - `docker run -d --name chat -p 8080:8080 -v :/usr/lib/python2.7/site-packages/LalkaChat/conf deforce/lalkachat` + - Open chat config at `http://localhost:10000/index.html?encoding=png&password=` + - And chat window at `http://localhost:8080/` + - You can update the chat with `docker rmi -f czt/lalkachat` + - Remove containers with `docker rmi -f x11-bridge chat` + + +#### Production docker build: + + - `docker build -t deforce/alpine-wxpython:latest docker/Dockerfiles/alpine-wxpython` + - `docker build -t deforce/lalkachat:latest .` + +#### Testing docker build: + - `docker build -t deforce/alpine-wxpython:latest docker/Dockerfiles/alpine-wxpython` + - `docker build -t deforce/lalkachat-build-deps:latest docker/Dockerfiles/lalkachat-build-deps` + - `docker run -d --name x11-bridge -e MODE="html" -p 10000:10000 -e XPRA_DISPLAY=:14 -e XPRA_PASSWORD= jare/x11-bridge` + - (on source change) + - `docker build -t deforce/lalkachat:testing -f Dockerfile_test .` + - `docker run -rm --name chat-test -p 8080:8080 -v :/usr/lib/python2.7/site-packages/LalkaChat/conf deforce/lalkachat:testing` + +## Special Thanks: +ftpud - for fixing IE compatibility (Old problem with IE Browser) +JAre - for being awesome with his docker stuff +ichursin - for deep knowledge in JavaScript and helping me with code +l0stparadis3 - for helping and testing in Linux environment diff --git a/docker/Dockerfiles/alpine-wxpython/Dockerfile b/docker/Dockerfiles/alpine-wxpython/Dockerfile new file mode 100644 index 0000000..56e6679 --- /dev/null +++ b/docker/Dockerfiles/alpine-wxpython/Dockerfile @@ -0,0 +1,28 @@ +FROM alpine:edge + +MAINTAINER CzT/DeForce + +# docker build --build-arg WXPY_SRC_URL=http://heanet.dl.sourceforge.net/project/wxpython/wxPython/3.0.1.0/wxPython-src-3.0.1.0.tar.bz2 -t deforce/alpine-wxpython:3.0.1.0 . +ARG WXPY_SRC_URL="http://heanet.dl.sourceforge.net/project/wxpython/wxPython/3.0.2.0/wxPython-src-3.0.2.0.tar.bz2" + +RUN echo "http://nl.alpinelinux.org/alpine/edge/main" \ + >> /etc/apk/repositories && \ + echo "http://nl.alpinelinux.org/alpine/edge/testing" \ + >> /etc/apk/repositories && \ + echo "http://nl.alpinelinux.org/alpine/edge/community" \ + >> /etc/apk/repositories && \ + + apk --update add font-misc-misc libgcc mesa-gl python2 wxgtk && \ + + apk --update add --virtual build-deps build-base \ + bzip2 libstdc++ python2-dev tar wget wxgtk-dev xz && \ + + wget -qO- "${WXPY_SRC_URL}" | tar xj -C /tmp/ && \ + cd /tmp/wxPython-src-* && \ + cd ./wxPython && \ + python ./setup.py build && \ + python ./setup.py install && \ + + apk del build-deps && \ + rm -rf /var/cache/* /tmp/* /var/log/* ~/.cache && \ + mkdir -p /var/cache/apk diff --git a/docker/Dockerfiles/lalkachat-build-deps/Dockerfile b/docker/Dockerfiles/lalkachat-build-deps/Dockerfile new file mode 100644 index 0000000..166310f --- /dev/null +++ b/docker/Dockerfiles/lalkachat-build-deps/Dockerfile @@ -0,0 +1,7 @@ +FROM deforce/alpine-wxpython:latest + +MAINTAINER CzT/DeForce + +RUN apk --update add build-base bzip2 git libstdc++ \ + openssl-dev py2-pip python2-dev tar wget wxgtk-dev xz && \ + pip install requests cherrypy ws4py irc semantic_version jinja2 diff --git a/docker/run b/docker/run new file mode 100755 index 0000000..a0e22d3 --- /dev/null +++ b/docker/run @@ -0,0 +1,30 @@ +#!/bin/sh + +webchatCF="/usr/lib/python2.7/site-packages/LalkaChat/conf/webchat.cfg" +globCF="/usr/lib/python2.7/site-packages/LalkaChat/conf/config.cfg" + +if [ -f $webchatCF ]; +then + sed -i 's/^host =.*$/host = 0.0.0.0/' $webchatCF +else + cat <<-EOF | tee > ${webchatCF} +[server] +host = 0.0.0.0 + +EOF +fi + +if [ -f $globCF ]; +then + sed -i 's/^show_browser =.*$/show_browser = False/' $globCF +else + cat <<-EOF | tee > ${globCF} +[gui] +show_browser = False + +EOF +fi + +cd /usr/lib/python2.7/site-packages/LalkaChat + +python ./main.py diff --git a/docker/stubs/cefpython3/__init__.py b/docker/stubs/cefpython3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker/stubs/cefpython3/wx/__init__.py b/docker/stubs/cefpython3/wx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker/stubs/cefpython3/wx/chromectrl.py b/docker/stubs/cefpython3/wx/chromectrl.py new file mode 100644 index 0000000..0dbe8c9 --- /dev/null +++ b/docker/stubs/cefpython3/wx/chromectrl.py @@ -0,0 +1,9 @@ +class ChromeCtrl: #temporary solution https://github.com/DeForce/LalkaChat/issues/154 + def __init__(self, parent, url="", useTimer=True, + timerMillis=0, + browserSettings=None, hasNavBar=True, + *args, **kwargs): + self.stub = True + +def Initialize(settings=None, debug=False): + pass diff --git a/gui.py b/gui.py index 661c407..d3ace48 100644 --- a/gui.py +++ b/gui.py @@ -1,14 +1,17 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov import threading from collections import OrderedDict +import os +import logging import webbrowser import wx import wx.grid -import os -import logging from ConfigParser import ConfigParser from cefpython3.wx import chromectrl from modules.helper.system import MODULE_KEY, translate_key +from modules.helper.parser import return_type +from modules.helper.module import BaseModule # ToDO: Support customization of borders/spacings # ToDO: Exit by cancel button @@ -18,7 +21,7 @@ SECTION_GUI_TAG = '__gui' SKIP_TAGS = [INFORMATION_TAG] SKIP_TXT_CONTROLS = ['list_input', 'list_input2'] -SKIP_BUTTONS = ['list_add', 'list_remove', 'apply_button', 'cancel_button'] +SKIP_BUTTONS = ['list_add', 'list_remove', 'apply_button', 'cancel_button', 'ok_button'] ITEM_SPACING_VERT = 6 ITEM_SPACING_HORZ = 30 @@ -27,8 +30,6 @@ def get_id_from_name(name, error=False): for item, item_id in IDS.iteritems(): if item_id == name: return item - if error: - raise ReferenceError return None @@ -146,14 +147,16 @@ def __init__(self, *args, **kwargs): self.settings_saved = True self.tree_ctrl = None self.content_page = None - self.sizer_dict = {} + self.sizer_list = [] + self.changes = {} + self.buttons = {} # Setting up the window self.SetBackgroundColour('cream') self.show_hidden = self.main_class.gui_settings.get('show_hidden') # Setting up events - self.Bind(wx.EVT_CLOSE, self.on_close_save) + self.Bind(wx.EVT_CLOSE, self.on_close) styles = wx.DEFAULT_FRAME_STYLE if wx.STAY_ON_TOP & self.main_class.GetWindowStyle() == wx.STAY_ON_TOP: @@ -168,31 +171,7 @@ def on_exit(self, event): self.Destroy() def on_close(self, event): - dialog = wx.MessageDialog(self, message=translate_key(MODULE_KEY.join(['main', 'quit'])), - caption="Caption", - style=wx.YES_NO | wx.CANCEL, - pos=wx.DefaultPosition) - response = dialog.ShowModal() - - if response == wx.ID_YES: - self.on_exit(event) - else: - event.StopPropagation() - - def on_close_save(self, event): - if not self.settings_saved: - dialog = wx.MessageDialog(self, message=translate_key(MODULE_KEY.join(['main', 'quit', 'nosave'])), - caption="Caption", - style=wx.YES_NO, - pos=wx.DefaultPosition) - response = dialog.ShowModal() - - if response == wx.ID_YES: - self.on_exit(event) - else: - event.StopPropagation() - else: - self.on_exit(event) + self.on_exit(event) def on_listbox_change(self, event): item_object = event.EventObject @@ -202,21 +181,118 @@ def on_listbox_change(self, event): item_key = IDS[event.GetId()].split(MODULE_KEY) show_description = self.main_class.loaded_modules[item_key[0]]['gui'][item_key[1]].get('description', False) + if isinstance(item_object, KeyListBox): + self.on_change(IDS[event.GetId()], selection, item_type='listbox', section=True) + if show_description: item_id_key = MODULE_KEY.join(item_key[:-1]) descr_static_text = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([item_id_key, 'descr_explain']))) descr_static_text.SetLabel(description) descr_static_text.Wrap(descr_static_text.GetSize()[0]) + def on_checklist_box_change(self, event): + window = event.EventObject + item_ids = window.GetChecked() + items_values = [window.get_key_from_index(item_id) for item_id in item_ids] + self.on_change(IDS[event.GetId()], items_values, item_type='listbox_check', section=True) + + def on_change(self, key, value, item_type=None, section=False): + def compare_2d_lists(list1, list2): + return not set(map(tuple, list1)) ^ set(map(tuple, list2)) + + def apply_changes(): + self.changes[key] = {'value': value, 'type': item_type} + self.buttons[MODULE_KEY.join(['settings', 'apply_button'])].Enable() + + def clear_changes(): + if key in self.changes: + self.changes.pop(key) + if not self.changes: + self.buttons[MODULE_KEY.join(['settings', 'apply_button'])].Disable() + split_keys = key.split(MODULE_KEY) + config = self.main_class.loaded_modules[split_keys[0]]['config'] + if section: + if isinstance(value, list): + if set(config[split_keys[1]].keys()) != set(value): + apply_changes() + else: + clear_changes() + else: + if config[split_keys[1]].decode('utf-8') != return_type(value).decode('utf-8'): + apply_changes() + else: + clear_changes() + elif item_type == 'gridbox': + main_tuple = [] + for item, i_value in self.main_class.loaded_modules[split_keys[0]]['config'][split_keys[1]].iteritems(): + if i_value: + main_tuple.append((item.decode('utf-8'), i_value.decode('utf-8'))) + else: + main_tuple.append((item.decode('utf-8'),)) + + if compare_2d_lists(value, main_tuple): + clear_changes() + else: + apply_changes() + else: + if isinstance(value, bool): + if config[split_keys[1]][split_keys[2]] != value: + apply_changes() + else: + clear_changes() + else: + if config[split_keys[1]][split_keys[2]] != return_type(value): + apply_changes() + else: + clear_changes() + + def on_tree_ctrl_changed(self, event): + self.settings_saved = False + tree_ctrl = event.EventObject # type: wx.TreeCtrl + selection = tree_ctrl.GetFocusedItem() + selection_text = tree_ctrl.GetItemData(selection).GetData() + key_list = selection_text.split(MODULE_KEY) + + # Drawing page + self.fill_page_with_content(self.content_page, key_list[1], key_list[-1], + self.main_class.loaded_modules[key_list[-1]]) + + event.Skip() + + def on_textctrl(self, event): + text_ctrl = event.EventObject + self.on_change(IDS[event.GetId()], text_ctrl.GetValue().encode('utf-8'), item_type='textctrl') + event.Skip() + + def on_spinctrl(self, event): + spin_ctrl = event.EventObject + self.on_change(IDS[event.GetId()], str(spin_ctrl.GetValue()), item_type='spinctrl') + event.Skip() + + def on_sliderctrl(self, event): + ctrl = event.EventObject + self.on_change(IDS[event.GetId()], str(ctrl.GetValue()), item_type='sliderctrl') + event.Skip() + + def on_dropdown(self, event): + drop_ctrl = event.EventObject + self.on_change(IDS[event.GetId()], drop_ctrl.GetString(drop_ctrl.GetCurrentSelection()), + item_type='dropctrl') + event.Skip() + + def on_check_change(self, event): + check_ctrl = event.EventObject + self.on_change(IDS[event.GetId()], check_ctrl.IsChecked(), item_type='checkbox') + event.Skip() + def create_layout(self): self.main_grid = wx.BoxSizer(wx.HORIZONTAL) - tree_ctrl_size = wx.Size(220, -1) style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.TR_TWIST_BUTTONS | wx.TR_NO_LINES # style = wx.TR_HAS_BUTTONS | wx.TR_SINGLE | wx.TR_HIDE_ROOT tree_ctrl_id = id_renew('settings.tree', update=True) tree_ctrl = wx.TreeCtrl(self, id=tree_ctrl_id, style=style) - tree_ctrl.SetMinSize(tree_ctrl_size) + tree_ctrl.SetQuickBestSize(False) root_key = MODULE_KEY.join(['settings', 'tree', 'root']) root_node = tree_ctrl.AddRoot(translate_key(root_key)) for item, value in self.categories.iteritems(): @@ -232,9 +308,10 @@ def create_layout(self): f_item_data.SetData(f_item_key) tree_ctrl.AppendItem(item_node, translate_key(f_item), data=f_item_data) tree_ctrl.ExpandAll() + tree_ctrl.SetMinSize(wx.Size(tree_ctrl.GetSize()[0] + 70, -1)) self.tree_ctrl = tree_ctrl - self.Bind(wx.EVT_TREE_SEL_CHANGED, self.tree_ctrl_changed, id=tree_ctrl_id) + self.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_ctrl_changed, id=tree_ctrl_id) self.main_grid.Add(self.tree_ctrl, 0, wx.EXPAND | wx.ALL, 7) content_page_id = id_renew(MODULE_KEY.join(['settings', 'content'])) @@ -246,31 +323,48 @@ def create_layout(self): tree_ctrl.SelectItem(tree_ctrl.GetFirstChild(root_node)[0]) def fill_page_with_content(self, panel, setting_category, category_item, category_config): - def create_button(button_key, function): + def create_button(button_key, function, enabled=True): button_id = id_renew(button_key, update=True) c_button = wx.Button(panel, id=button_id, label=translate_key(button_key)) + if not enabled: + c_button.Disable() + self.buttons[button_key] = c_button self.Bind(wx.EVT_BUTTON, function, id=button_id) return c_button page_sizer = panel.GetSizer() # type: wx.Sizer if not page_sizer: page_sizer = wx.BoxSizer(wx.VERTICAL) - else: - page_sizer.DeleteWindows() - - # Creating sizer for page - sizer = wx.BoxSizer(wx.VERTICAL) - # Window for settings - sizer.Add(self.fill_sc_with_config(panel, category_config, category_item), 1, wx.EXPAND) - # Buttons - button_sizer = wx.BoxSizer(wx.HORIZONTAL) - for button_name in ['apply_button', 'cancel_button']: - button_sizer.Add(create_button(MODULE_KEY.join([setting_category, category_item, button_name]), + panel.SetSizer(page_sizer) + # Buttons + button_sizer = wx.BoxSizer(wx.HORIZONTAL) + button_sizer.Add(create_button(MODULE_KEY.join(['settings', 'ok_button']), + self.button_clicked), 0, wx.ALIGN_RIGHT) + button_sizer.Add(create_button(MODULE_KEY.join(['settings', 'apply_button']), + self.button_clicked, enabled=False), 0, wx.ALIGN_RIGHT) + button_sizer.Add(create_button(MODULE_KEY.join(['settings', 'cancel_button']), self.button_clicked), 0, wx.ALIGN_RIGHT) - sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT) - page_sizer.Add(sizer, 1, wx.EXPAND) + page_sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4) + else: + children = page_sizer.GetChildren() + children_len = len(children) + for index, child in enumerate(children): + window = child.GetWindow() if child.GetWindow() else child.GetSizer() + if index < children_len - 1: + page_sizer.Hide(window) + + if category_item in self.sizer_list: + # +1 because we have button sizer + sizer_index = self.sizer_list.index(category_item) + page_sizer.Show(sizer_index) + else: + # Creating sizer for page + sizer = wx.BoxSizer(wx.VERTICAL) + # Window for settings + sizer.Add(self.fill_sc_with_config(panel, category_config, category_item), 1, wx.EXPAND) + page_sizer.Prepend(sizer, 1, wx.EXPAND) + self.sizer_list.insert(0, category_item) page_sizer.Layout() - panel.SetSizer(page_sizer) panel.Layout() def fill_sc_with_config(self, panel, category_config, category_item): @@ -296,6 +390,16 @@ def fill_sc_with_config(self, panel, category_config, category_item): page_sc_window.SetSizer(sizer) return page_sc_window + @staticmethod + def create_text(parent, key): + item_text = wx.StaticText(parent, label=translate_key(key)) + item_text_box_ver = wx.BoxSizer(wx.VERTICAL) + item_text_box_ver.Add(item_text, 0, wx.EXPAND) + + item_text_box_hor = wx.BoxSizer(wx.HORIZONTAL) + item_text_box_hor.Add(item_text, 1, wx.ALIGN_CENTER) + return item_text_box_hor + def create_items(self, parent, key, section, section_gui): sizer = wx.BoxSizer(wx.VERTICAL) view = section_gui.get('view', 'normal') @@ -403,14 +507,20 @@ def create_choose(self, parent, view, key, section, section_gui): if is_single: item_list_box = KeyListBox(parent, id=id_renew(item_key, update=True), keys=list_items, choices=translated_items if translated_items else list_items, style=style) + item_list_box.Bind(wx.EVT_LISTBOX, self.on_listbox_change) else: item_list_box = KeyCheckListBox(parent, id=id_renew(item_key, update=True), keys=list_items, choices=translated_items if translated_items else list_items) - self.Bind(wx.EVT_LISTBOX, self.on_listbox_change, item_list_box) + item_list_box.Bind(wx.EVT_LISTBOX, self.on_listbox_change) + item_list_box.Bind(wx.EVT_CHECKLISTBOX, self.on_checklist_box_change) section_for = section if not is_single else {section: None} if is_single: - [item_list_box.SetSelection(list_items.index(item)) for item, value in section_for.items()] + item, value = section_for.items()[0] + if item not in item_list_box.GetItems(): + item_list_box.SetSelection(0) + else: + item_list_box.SetSelection(list_items.index(item)) else: check_items = [list_items.index(item) for item, value in section_for.items()] item_list_box.SetChecked(check_items) @@ -434,24 +544,35 @@ def create_choose(self, parent, view, key, section, section_gui): return item_sizer def create_dropdown(self, parent, view, key, section, section_gui, section_item=False, short_key=None): - item_text = wx.StaticText(parent, label=translate_key(key), - style=wx.ALIGN_RIGHT) choices = section_gui.get('choices') key = key if section_item else MODULE_KEY.join([key, 'dropdown']) item_box = KeyChoice(parent, id=id_renew(key, update=True), keys=choices, choices=choices) + item_box.Bind(wx.EVT_CHOICE, self.on_dropdown) item_value = section[short_key] if section_item else section item_box.SetSelection(choices.index(item_value)) - return item_text, item_box + return self.create_text(parent, key), item_box def create_spin(self, parent, view, key, section, section_gui, section_item=False, short_key=None): - item_text = wx.StaticText(parent, label=translate_key(key), - style=wx.ALIGN_RIGHT) key = key if section_item else MODULE_KEY.join([key, 'spin']) value = short_key if section_item else section item_box = wx.SpinCtrl(parent, id=id_renew(key, update=True), min=section_gui['min'], max=section_gui['max'], - initial=value) - return item_text, item_box + initial=int(value)) + item_box.Bind(wx.EVT_SPINCTRL, self.on_spinctrl) + item_box.Bind(wx.EVT_TEXT, self.on_spinctrl) + return self.create_text(parent, key), item_box + + def create_slider(self, parent, view, key, section, section_gui, section_item=False, short_key=None): + key = key if section_item else MODULE_KEY.join([key, 'slider']) + value = short_key if section_item else section + item_box = wx.Slider(parent, id=id_renew(key, update=True), + minValue=section_gui['min'], maxValue=section_gui['max'], + value=int(value), style=wx.SL_LABELS | wx.SL_AUTOTICKS) + freq = (section_gui['max'] - section_gui['min'])/5 + item_box.SetTickFreq(freq) + item_box.SetLineSize(4) + item_box.Bind(wx.EVT_SCROLL, self.on_sliderctrl) + return self.create_text(parent, key), item_box def create_item(self, parent, view, key, section, section_gui): flex_grid = wx.FlexGridSizer(0, 2, ITEM_SPACING_VERT, ITEM_SPACING_HORZ) @@ -478,6 +599,11 @@ def create_item(self, parent, view, key, section, section_gui): section_item=True, short_key=section[item]) flex_grid.Add(text) flex_grid.Add(control) + elif 'slider' in section_gui[item].get('view'): + text, control = self.create_slider(parent, view, item_name, section, section_gui[item], + section_item=True, short_key=section[item]) + flex_grid.Add(text, 1, wx.EXPAND | wx.ALIGN_CENTER) + flex_grid.Add(control, 1, wx.EXPAND) else: # Checking type of an item style = wx.ALIGN_CENTER_VERTICAL @@ -491,14 +617,15 @@ def create_item(self, parent, view, key, section, section_gui): item_box = wx.CheckBox(parent, id=id_renew(item_name, update=True), label=translate_key(item_name), style=style) item_box.SetValue(value) + item_box.Bind(wx.EVT_CHECKBOX, self.on_check_change) flex_grid.Add(item_box, 0, wx.ALIGN_LEFT) flex_grid.AddSpacer(wx.Size(0, 0)) else: # TextCtrl item_box = wx.TextCtrl(parent, id=id_renew(item_name, update=True), value=str(value).decode('utf-8')) - item_text = wx.StaticText(parent, label=translate_key(item_name), - style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_HORIZONTAL) - flex_grid.Add(item_text) + item_box.Bind(wx.EVT_TEXT, self.on_textctrl) + item_text = self.create_text(parent, item_name) + flex_grid.Add(item_text, 1, wx.EXPAND | wx.ALIGN_CENTER) flex_grid.Add(item_box) flex_grid.Fit(parent) return flex_grid @@ -509,56 +636,47 @@ def button_clicked(self, event): keys = IDS[button_id].split(MODULE_KEY) if keys[-1] in ['list_add', 'list_remove']: self.list_operation(MODULE_KEY.join(keys[:-1]), action=keys[-1]) - elif keys[-1] == 'apply_button': - module_name = MODULE_KEY.join(keys[1:-1]) - if self.save_settings(module_name): + elif keys[-1] in ['ok_button', 'apply_button']: + if self.save_settings(): log.debug('Got non-dynamic changes') dialog = wx.MessageDialog(self, message=translate_key(MODULE_KEY.join(['main', 'save', 'non_dynamic'])), caption="Caption", style=wx.OK_DEFAULT, pos=wx.DefaultPosition) - response = dialog.ShowModal() - - if response == wx.ID_YES: - self.on_exit(event) - else: - event.StopPropagation() - module_class = self.main_class.loaded_modules[module_name].get('class') - if module_class: - module_class.apply_settings() + dialog.ShowModal() + if keys[-1] == 'ok_button': + self.on_exit(event) self.settings_saved = True elif keys[-1] == 'cancel_button': self.on_close(event) event.Skip() - def tree_ctrl_changed(self, event): - self.settings_saved = False - tree_ctrl = event.EventObject # type: wx.TreeCtrl - selection = tree_ctrl.GetFocusedItem() - selection_text = tree_ctrl.GetItemData(selection).GetData() - key_list = selection_text.split(MODULE_KEY) - - # Drawing page - self.fill_page_with_content(self.content_page, key_list[1], key_list[-1], - self.main_class.loaded_modules[key_list[-1]]) - - event.Skip() - - def save_settings(self, module): - module_settings = self.main_class.loaded_modules.get(module, {}) - non_dynamic = module_settings.get('gui', {}).get('non_dynamic', []) - module_config = module_settings.get('config') + def save_settings(self): + dynamic_check = False + for module in self.main_class.loaded_modules.keys(): + change_list = {} + for item, change in self.changes.iteritems(): + if module == item.split(MODULE_KEY)[0]: + change_list[item] = change + for key in change_list.keys(): + self.changes.pop(key) + + if self.save_module(module, change_list): + dynamic_check = True + self.buttons[MODULE_KEY.join(['settings', 'apply_button'])].Disable() + return dynamic_check + + def save_module(self, module, changed_items): non_dynamic_check = False - if module_settings: - parser = module_settings['parser'] # type: ConfigParser - items = get_list_of_ids_from_module_name(module, return_tuple=True) - for item, name in items: - module_name, section, item_name = name.split(MODULE_KEY) - - if not parser.has_section(section): - continue - # Check for non-dynamic items + if changed_items: + module_settings = self.main_class.loaded_modules.get(module, {}) + non_dynamic = module_settings.get('gui', {}).get('non_dynamic', []) + module_config = module_settings.get('config') + + for item, change in changed_items.iteritems(): + item_split = item.split(MODULE_KEY) + section, item_name = item_split[1:] if len(item_split) > 2 else (item_split[1], None) for d_item in non_dynamic: if section in d_item: if MODULE_KEY.join([section, '*']) in d_item: @@ -567,84 +685,28 @@ def save_settings(self, module): elif MODULE_KEY.join([section, item_name]) in d_item: non_dynamic_check = True break - # Saving - wx_window = wx.FindWindowById(item) - if isinstance(wx_window, wx.CheckBox): - if name == MODULE_KEY.join(['main', 'gui', 'show_hidden']): - self.show_hidden = wx_window.IsChecked() - parser.set(section, item_name, wx_window.IsChecked()) - module_config[section][item_name] = wx_window.IsChecked() - elif isinstance(wx_window, wx.TextCtrl): - if item_name not in SKIP_TXT_CONTROLS: - parser.set(section, item_name, wx_window.GetValue().encode('utf-8').strip()) - module_config[section][item_name] = wx_window.GetValue().encode('utf-8').strip() - elif isinstance(wx_window, wx.grid.Grid): - col_count = wx_window.GetNumberCols() - row_count = wx_window.GetNumberRows() - parser_options = parser.options(section) - grid_elements = [[wx_window.GetCellValue(row, col).encode('utf-8').strip() - for col in range(col_count)] - for row in range(row_count)] - if not grid_elements: - for option in parser_options: - parser.remove_option(section, option) - module_config[section].pop(option) - else: - item_list = [item[0] for item in grid_elements] - for option in parser_options: - if option not in item_list: - module_config[section].pop(option) - parser.remove_option(section, option) - for elements in grid_elements: - parser.set(section, *elements) - if len(elements) == 1: - module_config[section][elements[0]] = None - elif len(elements) == 2: - module_config[section][elements[0]] = elements[1] - elif isinstance(wx_window, wx.Button): - if item_name not in SKIP_BUTTONS: - parser.set(section, item_name) - module_config[section][item_name] = None - elif isinstance(wx_window, KeyListBox): - item_id = wx_window.GetSelection() - parser_options = parser.options(section) - item_value = wx_window.get_key_from_index(item_id) - if not item_value: - for option in parser_options: - parser.remove_option(section, option) - module_config[section] = None - else: - for option in parser_options: - parser.remove_option(section, option) - parser.set(section, item_value) - module_config[section] = item_value - elif isinstance(wx_window, KeyCheckListBox): - item_ids = wx_window.GetChecked() - parser_options = parser.options(section) - items_values = [wx_window.get_key_from_index(item_id) for item_id in item_ids] - if not items_values: - for option in parser_options: - parser.remove_option(section, option) - module_config[section].pop(option) - else: - for option in parser_options: - if option not in items_values: - parser.remove_option(section, option) - module_config[section].pop(option) - for value in items_values: - parser.set(section, value) - module_config[section][value] = None - elif isinstance(wx_window, KeyChoice): - item_id = wx_window.GetSelection() - item_value = wx_window.get_key_from_index(item_id) - parser.set(section, item_name, item_value) - module_config[section][item_name] = item_value - elif isinstance(wx_window, wx.SpinCtrl): - item_value = wx_window.GetValue() - parser.set(section, item_name, item_value) - module_config[section][item_name] = item_value - with open(module_settings['file'], 'w') as config_file: - parser.write(config_file) + change_type = change['type'] + if change_type in ['gridbox']: + module_config[section] = OrderedDict() + for change_tuple in change['value']: + if len(change_tuple) > 1: + change_item, change_value = change_tuple + else: + change_item, change_value = (change_tuple[0], None) + module_config[section][change_item] = change_value + elif change_type in ['listbox']: + module_config[section] = change['value'] + elif change_type in ['listbox_check']: + module_config[section] = {} + for value in change['value']: + module_config[section][value] = None + else: + value = change['value'] + if item == MODULE_KEY.join(['main', 'gui', 'show_hidden']): + self.show_hidden = value + module_config[section][item_name] = value + if 'class' in module_settings: + module_settings['class'].apply_settings() return non_dynamic_check def select_cell(self, event): @@ -652,24 +714,20 @@ def select_cell(self, event): event.Skip() def list_operation(self, key, action): + list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) if action == 'list_add': list_input_value = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_input']))).GetValue() - try: - list_input2 = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_input2']), error=True)) - list_input2_value = list_input2.GetValue() if list_input2 else None - except ReferenceError: - list_input2_value = None - - list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) list_box.AppendRows(1) row_count = list_box.GetNumberRows() - 1 - list_box.SetCellValue(row_count, 0, list_input_value.strip()) - if list_input2_value: + list_box.SetCellValue(row_count, 0, list_input_value.strip().lower()) + + list_input2_id = get_id_from_name(MODULE_KEY.join([key, 'list_input2'])) + if list_input2_id: + list_input2_value = wx.FindWindowById(list_input2_id).GetValue() list_box.SetCellValue(row_count, 1, list_input2_value.strip()) elif action == 'list_remove': - list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) top = list_box.GetSelectionBlockTopLeft() bot = list_box.GetSelectionBlockBottomRight() if top and bot: @@ -684,6 +742,88 @@ def list_operation(self, key, action): for select in del_rows: list_box.DeleteRows(select - ids_deleted) ids_deleted += 1 + rows = list_box.GetNumberRows() + cols = list_box.GetNumberCols() + grid_elements = [tuple([list_box.GetCellValue(row, col).encode('utf-8').strip() + for col in range(cols)]) + for row in range(rows)] + + self.on_change(key, grid_elements, item_type='gridbox') + + +class StatusFrame(wx.Panel): + def __init__(self, parent, **kwargs): + self.chat_modules = kwargs.get('chat_modules') + wx.Panel.__init__(self, parent, size=wx.Size(-1, 24)) + self.SetBackgroundColour('cream') + + self.chats = {} + self._create_items() + + self.Fit() + self.Layout() + self.Show(True) + + def _create_items(self): + border_sizer = wx.BoxSizer(wx.HORIZONTAL) + border_sizer.AddSpacer(5) + item_sizer = wx.FlexGridSizer(0, 0, 10, 10) + for module, settings in self.chat_modules.iteritems(): + if module not in ['chat']: + self.chats[module] = {} + item_sizer.Add(self._create_item(module, settings), 0, wx.EXPAND) + + border_sizer.Add(item_sizer, 0, wx.EXPAND) + border_sizer.AddSpacer(5) + self.SetSizer(border_sizer) + + def _create_item(self, module, settings): + item_sizer = wx.BoxSizer(wx.VERTICAL) + module_sizer = wx.FlexGridSizer(0, 0, 5, 5) + + chat_icon = wx.Image(settings['gui'].get('icon'), wx.BITMAP_TYPE_ANY) + chat_icon = chat_icon.Scale(16, 16) + bitmap = wx.StaticBitmap(self, wx.ID_ANY, + wx.BitmapFromImage(chat_icon), + size=wx.Size(16, 16)) + label = wx.StaticText(self, id=wx.ID_ANY, label='N/A') + self.chats[module]['label'] = label + module_sizer.Add(bitmap, 0, wx.EXPAND) + module_sizer.Add(label, 1, wx.EXPAND) + module_sizer.AddSpacer(2) + + item_sizer.Add(module_sizer) + + status_sizer = wx.BoxSizer(wx.HORIZONTAL) + status_item = wx.Panel(self, size=wx.Size(-1, 5)) + status_item.SetBackgroundColour('gray') + self.chats[module]['status'] = status_item + + status_sizer.Add(status_item, 1, wx.EXPAND) + + item_sizer.AddSpacer(3) + item_sizer.Add(status_sizer, 1, wx.EXPAND) + item_sizer.AddSpacer(2) + + return item_sizer + + def set_online(self, module): + self.chats[module]['status'].SetBackgroundColour(wx.Colour(0, 128, 0)) + self.Refresh() + + def set_offline(self, module): + self.chats[module]['status'].SetBackgroundColour('red') + self.Refresh() + + def set_viewers(self, module, viewers): + if not viewers: + return + if isinstance(viewers, int): + viewers = str(viewers) + if len(viewers) >= 5: + viewers = '{0}k'.format(viewers[:-3]) + self.chats[module]['label'].SetLabel(str(viewers)) + self.Layout() class ChatGui(wx.Frame): @@ -694,6 +834,7 @@ def __init__(self, parent, title, url, **kwargs): self.loaded_modules = kwargs.get('loaded_modules') self.queue = kwargs.get('queue') self.settings_window = None + self.status_frame = None wx.Frame.__init__(self, parent, title=title, size=self.gui_settings.get('size')) # Set window style @@ -711,16 +852,24 @@ def __init__(self, parent, title, url, **kwargs): # Creating main gui window vbox = wx.BoxSizer(wx.VERTICAL) self.toolbar = MainMenuToolBar(self, main_class=self) - self.browser_window = chromectrl.ChromeCtrl(self, useTimer=False, url=str(url), hasNavBar=False) vbox.Add(self.toolbar, 0, wx.EXPAND) - vbox.Add(self.browser_window, 1, wx.EXPAND) + if self.main_config['config']['gui']['show_counters']: + self.status_frame = StatusFrame(self, chat_modules=self.sorted_categories['chat']) + vbox.Add(self.status_frame, 0, wx.EXPAND) + if self.gui_settings['show_browser']: + vbox.Add(chromectrl.ChromeCtrl(self, useTimer=False, url=str(url), hasNavBar=False), 1, wx.EXPAND) # Set events self.Bind(wx.EVT_CLOSE, self.on_close) # Show window after creation self.SetSizer(vbox) + + if not self.gui_settings['show_browser']: + self.Layout() + self.Fit() + self.Show(True) # Show update dialog if new version found @@ -735,15 +884,21 @@ def __init__(self, parent, title, url, **kwargs): def on_close(self, event): log.info("Exiting...") + for module, module_dict in self.loaded_modules.iteritems(): + module_dict['class'].apply_settings(system_exit=True) + # Saving last window size parser = self.loaded_modules['main']['parser'] # type: ConfigParser size = self.Size parser.set('gui_information', 'width', size[0]) parser.set('gui_information', 'height', size[1]) - parser.write(open(self.loaded_modules['main']['file'], 'w')) + with open(self.loaded_modules['main']['file'], 'w') as p_file: + parser.write(p_file) + self.Destroy() - def on_right_down(self, event): + @staticmethod + def on_right_down(event): log.info(event) event.Skip() @@ -762,7 +917,8 @@ def on_settings(self, event): main_class=self, categories=self.sorted_categories) - def button_clicked(self, event): + @staticmethod + def button_clicked(event): button_id = event.GetId() keys = IDS[event.GetId()].split(MODULE_KEY) log.debug("[ChatGui] Button clicked: {0}, {1}".format(keys, button_id)) @@ -781,13 +937,14 @@ def on_toolbar_button(self, event): event.Skip() -class GuiThread(threading.Thread): +class GuiThread(threading.Thread, BaseModule): title = 'LalkaChat' url = 'http://localhost' port = '8080' def __init__(self, **kwargs): threading.Thread.__init__(self) + BaseModule.__init__(self, **kwargs) self.daemon = True self.gui = None self.kwargs = kwargs diff --git a/http/czt/css/style.css b/http/czt/css/style.css deleted file mode 100644 index b43106f..0000000 --- a/http/czt/css/style.css +++ /dev/null @@ -1,77 +0,0 @@ -::-webkit-scrollbar { - visibility: hidden; -} -body{ - margin: 0; -} -#ChatContainer{ - position: absolute; - bottom: 0; - width: 100%; -} -.msg{ - background-color: rgba( 35, 35, 37, 0.627451 ); - font-family: 'Consolas', serif; - font-size: {{ font_size }}pt; - text-shadow: 0 1px 1px black; - padding-top: 2px; - word-wrap: break-word; - color: #FFFFFF; -} -.msgSource, -.msgBadge, -.msgLevel, -.msgSLevel{ - width: 16px; - height: 16px; - background-color: rgba(0,0,0,0.6); - border-radius: 3px; - margin: 0 3px 0 0; - position: relative; - top: 3px; - left: 3px; - display: inline-block; -} -.msgUser{ - display: inline-block; - position: relative; - padding-left: 5px; -} -.msgText{ - display: inline; - height: auto; - padding-left: 5px; -} -.msgTextPriv{ - display: inline; - height: auto; - padding-left: 5px; - color: #e57017; -} -.msgTextMention{ - display: inline; - height: auto; - padding-left: 5px; - color: #c0ffc0; -} -.msgTextSystem{ - display: inline; - height: auto; - padding-left: 5px; - color: #ff68fb; -} -.imgSmile{ - margin-top: -4px; - height: 20px; - width: auto; - top: 2px; - position: relative; -} -.imgBadge, -.imgSource, -.imgLevel, -.imgSLevel{ - width: 16px; - height: 16px; - border-radius: 3px; -} \ No newline at end of file diff --git a/http/czt/index.html b/http/czt/index.html deleted file mode 100644 index cf8d366..0000000 --- a/http/czt/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - LalkaChat - - - - - - - - -
- diff --git a/http/czt/js/socket.js b/http/czt/js/socket.js deleted file mode 100644 index 2493070..0000000 --- a/http/czt/js/socket.js +++ /dev/null @@ -1,275 +0,0 @@ -var MAX_MESSAGES = 70; -var find_location = window.location.href; -var RegExp = /:(\d+)/; -var find_list = RegExp.exec(find_location.toString()); -var find_port = find_list[1]; -var ws_url = "ws://127.0.0.1:".concat(find_port, "/ws"); - -// Chat settings -var timeout = 0; -var loadHistory = true; - -var socket = new WebSocket(ws_url); - -var chatMessages; -socket.onopen = function() { - console.log("Socket connected") - chatMessages = document.getElementById('ChatContainer'); -}; - -socket.onclose = function(event) { - if (event.wasClean) { - console.log("Socket closed cleanly") - } - else { - console.log("Socket closed not cleanly") - } -}; - -socket.onmessage = function(event) { - var incomingMessage = JSON.parse(event.data); - if(incomingMessage.hasOwnProperty('command')) { - runCommand(incomingMessage); - } - else { - if (loadHistory) { - showMessage(incomingMessage); - } - else if (!incomingMessage.hasOwnProperty('history')) { - showMessage(incomingMessage); - } - } -}; - -socket.onerror = function(error) { -}; - -twitch_processEmoticons = function(message, emotes) { - if (!emotes) { - return message; - } - var placesToReplace = []; - for (var emote in emotes) { - for (var i = 0; i < emotes[emote]['emote_pos'].length; ++i) { - var range = emotes[emote]['emote_pos'][i]; - var rangeParts = range.split('-'); - placesToReplace.push({ - "emote_id": emotes[emote]['emote_id'], - "from": parseInt(rangeParts[0]), - "to": parseInt(rangeParts[1]) + 1 - }); - } - } - placesToReplace.sort(function(first, second) { - return second.from - first.from; - }); - for (var iPlace = 0; iPlace < placesToReplace.length; ++iPlace) { - var place = placesToReplace[iPlace]; - var emoticonRegex = message.substring(place.from, place.to); - // var url = "http://static-cdn.jtvnw.net/emoticons/v1/" + place.emote_id + "/1.0" - message = message.substring(0, place.from) + "$emoticon#" + place.emote_id + "$" + message.substring(place.to); - } - - return message; -}; - -htmlifyGGEmoticons = function(message, emotes) { - return message.replace(/:(\w+|\d+):/g, function (code, emote_key) { - for(var emote in emotes) { - if(emote_key == emotes[emote]['emote_id']) { - return ""; - } - } - return code; - }); -}; - -htmlifyBTTVEmoticons = function(message, emotes) { - return message.replace(/(^| )?(\S+)?( |$)/g, function (code, b1, emote_key, b2) { - for(var emote in emotes) { - if(emote_key == emotes[emote]['emote_id']) { - return ""; - } - } - return code; - }); -}; - -htmlifyTwitchEmoticons = function(message) { - return message.replace(/\$emoticon#(\d+)\$/g, function (code, emoteId) { - return ""; - }); -}; - -escapeHtml = (function () { - 'use strict'; - var chr = { '"': '"', '&': '&', '<': '<', '>': '>' }; - return function (text) { - return text.replace(/[\"&<>]/g, function (a) { return chr[a]; }); - }; -}()); - -function removeMessage(element) { - var elm = element || chatMessages.lastChild; - chatMessages.removeChild(elm); - } - -function updateMessages() { - if(chatMessages.children.length < MAX_MESSAGES) return; - var element = chatMessages.lastChild; - if(element.hasAttribute('timer-id')) { - var timerId = element.getAttribute('timer-id'); - window.clearTimeout(timerId); - } - removeMessage(element); - } - - -function showMessage(message) { - var badge_colors = 1; - - var elements = {}; - elements['message'] = document.createElement('div'); - elements.message.setAttribute('class', 'msg'); - if(timeout > 0) { - elements.message.setAttribute('timer-id', setTimeout(removeMessage, timeout * 1000, elements.message)); - } - - var messageJSON = message; - - if(messageJSON.hasOwnProperty('source')) { - //console.log("message has source " + messageJSON.source); - - elements.message['source'] = document.createElement('div'); - elements.message.source.setAttribute('class', 'msgSource'); - - elements.message.source['img'] = document.createElement('img'); - if(messageJSON.hasOwnProperty('source_icon')) { - elements.message.source.img.setAttribute('src', messageJSON.source_icon); - } - else{ - elements.message.source.img.setAttribute('src', '/img/sources/' + messageJSON.source + '.png'); - } - elements.message.source.img.setAttribute('class', 'imgSource'); - - elements.message.source.appendChild(elements.message.source.img); - elements.message.appendChild(elements.message.source); - } - - if(messageJSON.hasOwnProperty('levels')) { - elements.message['level'] = document.createElement('div'); - elements.message.level.setAttribute('class', 'msgLevel'); - - elements.message.level['img'] = document.createElement('img'); - elements.message.level.img.setAttribute('class', 'imgLevel'); - elements.message.level.img.setAttribute('src', messageJSON.levels.url); - - elements.message.level.appendChild(elements.message.level.img); - elements.message.appendChild(elements.message.level); - } - - if(messageJSON.hasOwnProperty('s_levels')) { - - for (i = 0; i < messageJSON.s_levels.length; i++) { - elements.message['s_level'] = document.createElement('div'); - elements.message.s_level.setAttribute('class', 'msgSLevel'); - - elements.message.s_level['img'] = document.createElement('img'); - elements.message.s_level.img.setAttribute('class', 'imgSLevel'); - elements.message.s_level.img.setAttribute('src', messageJSON.s_levels[i].url); - - elements.message.s_level.appendChild(elements.message.s_level.img); - elements.message.appendChild(elements.message.s_level); - } - } - - if(messageJSON.hasOwnProperty('badges')) { - - for (i = 0; i < messageJSON.badges.length; i++) { - elements.message['badge'] = document.createElement('div'); - elements.message.badge.setAttribute('class', 'msgBadge'); - - elements.message.badge['img'] = document.createElement('img'); - elements.message.badge.img.setAttribute('class', 'imgBadge'); - elements.message.badge.img.setAttribute('src', messageJSON.badges[i].url); - - if(badge_colors) { - if(messageJSON.badges[i].badge == 'broadcaster') { - elements.message.badge.img.setAttribute('style', 'background-color: #e71818'); - } - else if(messageJSON.badges[i].badge == 'mod') { - elements.message.badge.img.setAttribute('style', 'background-color: #34ae0a'); - } - else if(messageJSON.badges[i].badge == 'turbo') { - elements.message.badge.img.setAttribute('style', 'background-color: #6441a5'); - } - } - elements.message.badge.appendChild(elements.message.badge.img); - elements.message.appendChild(elements.message.badge); - } - } - - if(messageJSON.hasOwnProperty('user')) { - // console.log("message has user " + messageJSON.user); - elements.message['user'] = document.createElement('div'); - elements.message.user.setAttribute('class', 'msgUser'); - var addString = messageJSON.user; - - if (messageJSON.hasOwnProperty('msg_type')) { - if (messageJSON.msg_type == 'pubmsg') { - addString += ": " - } - } - else { - addString += ": " - } - - elements.message.user.appendChild(document.createTextNode(addString)); - - elements.message.appendChild(elements.message.user); - } - - if(messageJSON.hasOwnProperty('text')) { - // console.log("message has text " + messageJSON.text); - elements.message['text'] = document.createElement('div'); - if(messageJSON.source == 'sy') { - elements.message.text.setAttribute('class', 'msgTextSystem'); - } - else if(messageJSON.hasOwnProperty('pm') && messageJSON.pm == true) { - elements.message.text.setAttribute('class', 'msgTextPriv'); - } - else if(messageJSON.hasOwnProperty('mention') && messageJSON.mention == true){ - elements.message.text.setAttribute('class', 'msgTextMention'); - } - else { - elements.message.text.setAttribute('class', 'msgText'); - } - - if(messageJSON.source == 'tw') { - messageJSON.text = htmlifyTwitchEmoticons(escapeHtml(twitch_processEmoticons(messageJSON.text, messageJSON.emotes))); - if(messageJSON.hasOwnProperty('bttv_emotes')) { - messageJSON.text = htmlifyBTTVEmoticons(messageJSON.text, messageJSON.bttv_emotes); - } - } - else if(messageJSON.source == 'gg') { - messageJSON.text = htmlifyGGEmoticons(messageJSON.text, messageJSON.emotes) - } - else if(messageJSON.source == 'fs') { - messageJSON.text = htmlifyGGEmoticons(escapeHtml(messageJSON.text), messageJSON.emotes) - } - - // elements.message.text.appendChild(document.createTextNode(messageJSON.text)); - elements.message.text.innerHTML = messageJSON.text; - - elements.message.appendChild(elements.message.text); - - } - document.getElementById('ChatContainer').appendChild(elements.message); - // updateMessages(); -} - -function runCommand(message) { - if(message.command == 'reload'){ - window.location.reload(); - } -} \ No newline at end of file diff --git a/http/czt_timed/css/style.css b/http/czt_timed/css/style.css deleted file mode 100644 index b43106f..0000000 --- a/http/czt_timed/css/style.css +++ /dev/null @@ -1,77 +0,0 @@ -::-webkit-scrollbar { - visibility: hidden; -} -body{ - margin: 0; -} -#ChatContainer{ - position: absolute; - bottom: 0; - width: 100%; -} -.msg{ - background-color: rgba( 35, 35, 37, 0.627451 ); - font-family: 'Consolas', serif; - font-size: {{ font_size }}pt; - text-shadow: 0 1px 1px black; - padding-top: 2px; - word-wrap: break-word; - color: #FFFFFF; -} -.msgSource, -.msgBadge, -.msgLevel, -.msgSLevel{ - width: 16px; - height: 16px; - background-color: rgba(0,0,0,0.6); - border-radius: 3px; - margin: 0 3px 0 0; - position: relative; - top: 3px; - left: 3px; - display: inline-block; -} -.msgUser{ - display: inline-block; - position: relative; - padding-left: 5px; -} -.msgText{ - display: inline; - height: auto; - padding-left: 5px; -} -.msgTextPriv{ - display: inline; - height: auto; - padding-left: 5px; - color: #e57017; -} -.msgTextMention{ - display: inline; - height: auto; - padding-left: 5px; - color: #c0ffc0; -} -.msgTextSystem{ - display: inline; - height: auto; - padding-left: 5px; - color: #ff68fb; -} -.imgSmile{ - margin-top: -4px; - height: 20px; - width: auto; - top: 2px; - position: relative; -} -.imgBadge, -.imgSource, -.imgLevel, -.imgSLevel{ - width: 16px; - height: 16px; - border-radius: 3px; -} \ No newline at end of file diff --git a/http/czt_timed/img/levels/0.png b/http/czt_timed/img/levels/0.png deleted file mode 100644 index 5ca9d8a..0000000 Binary files a/http/czt_timed/img/levels/0.png and /dev/null differ diff --git a/http/czt_timed/img/levels/1.png b/http/czt_timed/img/levels/1.png deleted file mode 100644 index 66aa505..0000000 Binary files a/http/czt_timed/img/levels/1.png and /dev/null differ diff --git a/http/czt_timed/img/levels/10.png b/http/czt_timed/img/levels/10.png deleted file mode 100644 index 1ea1251..0000000 Binary files a/http/czt_timed/img/levels/10.png and /dev/null differ diff --git a/http/czt_timed/img/levels/11.png b/http/czt_timed/img/levels/11.png deleted file mode 100644 index a22762c..0000000 Binary files a/http/czt_timed/img/levels/11.png and /dev/null differ diff --git a/http/czt_timed/img/levels/12.png b/http/czt_timed/img/levels/12.png deleted file mode 100644 index 8c94a59..0000000 Binary files a/http/czt_timed/img/levels/12.png and /dev/null differ diff --git a/http/czt_timed/img/levels/13.png b/http/czt_timed/img/levels/13.png deleted file mode 100644 index 6878638..0000000 Binary files a/http/czt_timed/img/levels/13.png and /dev/null differ diff --git a/http/czt_timed/img/levels/14.png b/http/czt_timed/img/levels/14.png deleted file mode 100644 index c08dd39..0000000 Binary files a/http/czt_timed/img/levels/14.png and /dev/null differ diff --git a/http/czt_timed/img/levels/15.png b/http/czt_timed/img/levels/15.png deleted file mode 100644 index 64353ec..0000000 Binary files a/http/czt_timed/img/levels/15.png and /dev/null differ diff --git a/http/czt_timed/img/levels/16.png b/http/czt_timed/img/levels/16.png deleted file mode 100644 index eaab90e..0000000 Binary files a/http/czt_timed/img/levels/16.png and /dev/null differ diff --git a/http/czt_timed/img/levels/17.png b/http/czt_timed/img/levels/17.png deleted file mode 100644 index 682e71e..0000000 Binary files a/http/czt_timed/img/levels/17.png and /dev/null differ diff --git a/http/czt_timed/img/levels/18.png b/http/czt_timed/img/levels/18.png deleted file mode 100644 index 246369a..0000000 Binary files a/http/czt_timed/img/levels/18.png and /dev/null differ diff --git a/http/czt_timed/img/levels/19.png b/http/czt_timed/img/levels/19.png deleted file mode 100644 index af9497d..0000000 Binary files a/http/czt_timed/img/levels/19.png and /dev/null differ diff --git a/http/czt_timed/img/levels/2.png b/http/czt_timed/img/levels/2.png deleted file mode 100644 index 99bde2d..0000000 Binary files a/http/czt_timed/img/levels/2.png and /dev/null differ diff --git a/http/czt_timed/img/levels/20.png b/http/czt_timed/img/levels/20.png deleted file mode 100644 index 0216450..0000000 Binary files a/http/czt_timed/img/levels/20.png and /dev/null differ diff --git a/http/czt_timed/img/levels/21.png b/http/czt_timed/img/levels/21.png deleted file mode 100644 index d4b185e..0000000 Binary files a/http/czt_timed/img/levels/21.png and /dev/null differ diff --git a/http/czt_timed/img/levels/22.png b/http/czt_timed/img/levels/22.png deleted file mode 100644 index 6939a2f..0000000 Binary files a/http/czt_timed/img/levels/22.png and /dev/null differ diff --git a/http/czt_timed/img/levels/23.png b/http/czt_timed/img/levels/23.png deleted file mode 100644 index 9db3a5d..0000000 Binary files a/http/czt_timed/img/levels/23.png and /dev/null differ diff --git a/http/czt_timed/img/levels/24.png b/http/czt_timed/img/levels/24.png deleted file mode 100644 index 3dcb03e..0000000 Binary files a/http/czt_timed/img/levels/24.png and /dev/null differ diff --git a/http/czt_timed/img/levels/25.png b/http/czt_timed/img/levels/25.png deleted file mode 100644 index db09752..0000000 Binary files a/http/czt_timed/img/levels/25.png and /dev/null differ diff --git a/http/czt_timed/img/levels/26.png b/http/czt_timed/img/levels/26.png deleted file mode 100644 index a492bad..0000000 Binary files a/http/czt_timed/img/levels/26.png and /dev/null differ diff --git a/http/czt_timed/img/levels/27.png b/http/czt_timed/img/levels/27.png deleted file mode 100644 index 75e398d..0000000 Binary files a/http/czt_timed/img/levels/27.png and /dev/null differ diff --git a/http/czt_timed/img/levels/28.png b/http/czt_timed/img/levels/28.png deleted file mode 100644 index 2d01db2..0000000 Binary files a/http/czt_timed/img/levels/28.png and /dev/null differ diff --git a/http/czt_timed/img/levels/29.png b/http/czt_timed/img/levels/29.png deleted file mode 100644 index 9e78222..0000000 Binary files a/http/czt_timed/img/levels/29.png and /dev/null differ diff --git a/http/czt_timed/img/levels/3.png b/http/czt_timed/img/levels/3.png deleted file mode 100644 index c5b451d..0000000 Binary files a/http/czt_timed/img/levels/3.png and /dev/null differ diff --git a/http/czt_timed/img/levels/30.png b/http/czt_timed/img/levels/30.png deleted file mode 100644 index 33ab473..0000000 Binary files a/http/czt_timed/img/levels/30.png and /dev/null differ diff --git a/http/czt_timed/img/levels/31.png b/http/czt_timed/img/levels/31.png deleted file mode 100644 index b71b907..0000000 Binary files a/http/czt_timed/img/levels/31.png and /dev/null differ diff --git a/http/czt_timed/img/levels/32.png b/http/czt_timed/img/levels/32.png deleted file mode 100644 index 6282eea..0000000 Binary files a/http/czt_timed/img/levels/32.png and /dev/null differ diff --git a/http/czt_timed/img/levels/33.png b/http/czt_timed/img/levels/33.png deleted file mode 100644 index ad748bb..0000000 Binary files a/http/czt_timed/img/levels/33.png and /dev/null differ diff --git a/http/czt_timed/img/levels/34.png b/http/czt_timed/img/levels/34.png deleted file mode 100644 index fa98e40..0000000 Binary files a/http/czt_timed/img/levels/34.png and /dev/null differ diff --git a/http/czt_timed/img/levels/35.png b/http/czt_timed/img/levels/35.png deleted file mode 100644 index a620df4..0000000 Binary files a/http/czt_timed/img/levels/35.png and /dev/null differ diff --git a/http/czt_timed/img/levels/36.png b/http/czt_timed/img/levels/36.png deleted file mode 100644 index 4f1db46..0000000 Binary files a/http/czt_timed/img/levels/36.png and /dev/null differ diff --git a/http/czt_timed/img/levels/37.png b/http/czt_timed/img/levels/37.png deleted file mode 100644 index d216a83..0000000 Binary files a/http/czt_timed/img/levels/37.png and /dev/null differ diff --git a/http/czt_timed/img/levels/38.png b/http/czt_timed/img/levels/38.png deleted file mode 100644 index 600785d..0000000 Binary files a/http/czt_timed/img/levels/38.png and /dev/null differ diff --git a/http/czt_timed/img/levels/39.png b/http/czt_timed/img/levels/39.png deleted file mode 100644 index c3524f5..0000000 Binary files a/http/czt_timed/img/levels/39.png and /dev/null differ diff --git a/http/czt_timed/img/levels/4.png b/http/czt_timed/img/levels/4.png deleted file mode 100644 index 80a6b2e..0000000 Binary files a/http/czt_timed/img/levels/4.png and /dev/null differ diff --git a/http/czt_timed/img/levels/40.png b/http/czt_timed/img/levels/40.png deleted file mode 100644 index 5108e46..0000000 Binary files a/http/czt_timed/img/levels/40.png and /dev/null differ diff --git a/http/czt_timed/img/levels/41.png b/http/czt_timed/img/levels/41.png deleted file mode 100644 index 0088095..0000000 Binary files a/http/czt_timed/img/levels/41.png and /dev/null differ diff --git a/http/czt_timed/img/levels/42.png b/http/czt_timed/img/levels/42.png deleted file mode 100644 index 3a48399..0000000 Binary files a/http/czt_timed/img/levels/42.png and /dev/null differ diff --git a/http/czt_timed/img/levels/43.png b/http/czt_timed/img/levels/43.png deleted file mode 100644 index a68a199..0000000 Binary files a/http/czt_timed/img/levels/43.png and /dev/null differ diff --git a/http/czt_timed/img/levels/5.png b/http/czt_timed/img/levels/5.png deleted file mode 100644 index 1887163..0000000 Binary files a/http/czt_timed/img/levels/5.png and /dev/null differ diff --git a/http/czt_timed/img/levels/6.png b/http/czt_timed/img/levels/6.png deleted file mode 100644 index 60d5b33..0000000 Binary files a/http/czt_timed/img/levels/6.png and /dev/null differ diff --git a/http/czt_timed/img/levels/7.png b/http/czt_timed/img/levels/7.png deleted file mode 100644 index 85df85b..0000000 Binary files a/http/czt_timed/img/levels/7.png and /dev/null differ diff --git a/http/czt_timed/img/levels/8.png b/http/czt_timed/img/levels/8.png deleted file mode 100644 index 68ace6e..0000000 Binary files a/http/czt_timed/img/levels/8.png and /dev/null differ diff --git a/http/czt_timed/img/levels/9.png b/http/czt_timed/img/levels/9.png deleted file mode 100644 index 8b33420..0000000 Binary files a/http/czt_timed/img/levels/9.png and /dev/null differ diff --git a/http/czt_timed/img/levels/cube.png b/http/czt_timed/img/levels/cube.png deleted file mode 100644 index 7d05d56..0000000 Binary files a/http/czt_timed/img/levels/cube.png and /dev/null differ diff --git a/http/czt_timed/img/sources/fs.png b/http/czt_timed/img/sources/fs.png deleted file mode 100644 index b2a2721..0000000 Binary files a/http/czt_timed/img/sources/fs.png and /dev/null differ diff --git a/http/czt_timed/index.html b/http/czt_timed/index.html deleted file mode 100644 index 72ee1e2..0000000 --- a/http/czt_timed/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - LalkaChat - - - - - - - - -
- diff --git a/http/czt_timed/js/socket.js b/http/czt_timed/js/socket.js deleted file mode 100644 index 79b1d22..0000000 --- a/http/czt_timed/js/socket.js +++ /dev/null @@ -1,271 +0,0 @@ -var MAX_MESSAGES = 70; -var find_location = window.location.href; -var RegExp = /:(\d+)/; -var find_list = RegExp.exec(find_location.toString()); -var find_port = find_list[1]; -var ws_url = "ws://127.0.0.1:".concat(find_port, "/ws"); - -// Chat settings -var timeout = 180; -var loadHistory = false; - -var socket = new WebSocket(ws_url); - -var chatMessages; -socket.onopen = function() { - chatMessages = document.getElementById('ChatContainer'); -}; - -socket.onclose = function(event) { - if (event.wasClean) { - } else { - } -}; - -socket.onmessage = function(event) { - var incomingMessage = JSON.parse(event.data); - if(incomingMessage.hasOwnProperty('command')) { - runCommand(incomingMessage); - } - else { - if (loadHistory) { - showMessage(incomingMessage); - } - else if (!incomingMessage.hasOwnProperty('history')) { - showMessage(incomingMessage); - } - } -}; - -socket.onerror = function(error) { -}; - -twitch_processEmoticons = function(message, emotes) { - if (!emotes) { - return message; - } - var placesToReplace = []; - for (var emote in emotes) { - for (var i = 0; i < emotes[emote]['emote_pos'].length; ++i) { - var range = emotes[emote]['emote_pos'][i]; - var rangeParts = range.split('-'); - placesToReplace.push({ - "emote_id": emotes[emote]['emote_id'], - "from": parseInt(rangeParts[0]), - "to": parseInt(rangeParts[1]) + 1 - }); - } - } - placesToReplace.sort(function(first, second) { - return second.from - first.from; - }); - for (var iPlace = 0; iPlace < placesToReplace.length; ++iPlace) { - var place = placesToReplace[iPlace]; - var emoticonRegex = message.substring(place.from, place.to); - // var url = "http://static-cdn.jtvnw.net/emoticons/v1/" + place.emote_id + "/1.0" - message = message.substring(0, place.from) + "$emoticon#" + place.emote_id + "$" + message.substring(place.to); - } - - return message; -}; - -htmlifyGGEmoticons = function(message, emotes) { - return message.replace(/:(\w+|\d+):/g, function (code, emote_key) { - for(var emote in emotes) { - if(emote_key == emotes[emote]['emote_id']) { - return ""; - } - } - return code; - }); -}; - -htmlifyBTTVEmoticons = function(message, emotes) { - return message.replace(/(^| )?(\S+)?( |$)/g, function (code, b1, emote_key, b2) { - for(var emote in emotes) { - if(emote_key == emotes[emote]['emote_id']) { - return ""; - } - } - return code; - }); -}; - -htmlifyTwitchEmoticons = function(message) { - return message.replace(/\$emoticon#(\d+)\$/g, function (code, emoteId) { - return ""; - }); -}; - -escapeHtml = (function () { - 'use strict'; - var chr = { '"': '"', '&': '&', '<': '<', '>': '>' }; - return function (text) { - return text.replace(/[\"&<>]/g, function (a) { return chr[a]; }); - }; -}()); - -function removeMessage(element) { - var elm = element || chatMessages.lastChild; - chatMessages.removeChild(elm); - } - -function updateMessages() { - if(chatMessages.children.length < MAX_MESSAGES) return; - var element = chatMessages.lastChild; - if(element.hasAttribute('timer-id')) { - var timerId = element.getAttribute('timer-id'); - window.clearTimeout(timerId); - } - removeMessage(element); - } - - -function showMessage(message) { - var badge_colors = 1; - - var elements = {}; - elements['message'] = document.createElement('div'); - elements.message.setAttribute('class', 'msg'); - if(timeout > 0) { - elements.message.setAttribute('timer-id', setTimeout(removeMessage, timeout * 1000, elements.message)); - } - - var messageJSON = message; - - if(messageJSON.hasOwnProperty('source')) { - //console.log("message has source " + messageJSON.source); - - elements.message['source'] = document.createElement('div'); - elements.message.source.setAttribute('class', 'msgSource'); - - elements.message.source['img'] = document.createElement('img'); - if(messageJSON.hasOwnProperty('source_icon')) { - elements.message.source.img.setAttribute('src', messageJSON.source_icon); - } - else{ - elements.message.source.img.setAttribute('src', '/img/sources/' + messageJSON.source + '.png'); - } - elements.message.source.img.setAttribute('class', 'imgSource'); - - elements.message.source.appendChild(elements.message.source.img); - elements.message.appendChild(elements.message.source); - } - - if(messageJSON.hasOwnProperty('levels')) { - elements.message['level'] = document.createElement('div'); - elements.message.level.setAttribute('class', 'msgLevel'); - - elements.message.level['img'] = document.createElement('img'); - elements.message.level.img.setAttribute('class', 'imgLevel'); - elements.message.level.img.setAttribute('src', messageJSON.levels.url); - - elements.message.level.appendChild(elements.message.level.img); - elements.message.appendChild(elements.message.level); - } - - if(messageJSON.hasOwnProperty('s_levels')) { - - for (i = 0; i < messageJSON.s_levels.length; i++) { - elements.message['s_level'] = document.createElement('div'); - elements.message.s_level.setAttribute('class', 'msgSLevel'); - - elements.message.s_level['img'] = document.createElement('img'); - elements.message.s_level.img.setAttribute('class', 'imgSLevel'); - elements.message.s_level.img.setAttribute('src', messageJSON.s_levels[i].url); - - elements.message.s_level.appendChild(elements.message.s_level.img); - elements.message.appendChild(elements.message.s_level); - } - } - - if(messageJSON.hasOwnProperty('badges')) { - - for (i = 0; i < messageJSON.badges.length; i++) { - elements.message['badge'] = document.createElement('div'); - elements.message.badge.setAttribute('class', 'msgBadge'); - - elements.message.badge['img'] = document.createElement('img'); - elements.message.badge.img.setAttribute('class', 'imgBadge'); - elements.message.badge.img.setAttribute('src', messageJSON.badges[i].url); - - if(badge_colors) { - if(messageJSON.badges[i].badge == 'broadcaster') { - elements.message.badge.img.setAttribute('style', 'background-color: #e71818'); - } - else if(messageJSON.badges[i].badge == 'mod') { - elements.message.badge.img.setAttribute('style', 'background-color: #34ae0a'); - } - else if(messageJSON.badges[i].badge == 'turbo') { - elements.message.badge.img.setAttribute('style', 'background-color: #6441a5'); - } - } - elements.message.badge.appendChild(elements.message.badge.img); - elements.message.appendChild(elements.message.badge); - } - } - - if(messageJSON.hasOwnProperty('user')) { - // console.log("message has user " + messageJSON.user); - elements.message['user'] = document.createElement('div'); - elements.message.user.setAttribute('class', 'msgUser'); - var addString = messageJSON.user; - - if (messageJSON.hasOwnProperty('msg_type')) { - if (messageJSON.msg_type == 'pubmsg') { - addString += ": " - } - } - else { - addString += ": " - } - - elements.message.user.appendChild(document.createTextNode(addString)); - - elements.message.appendChild(elements.message.user); - } - - if(messageJSON.hasOwnProperty('text')) { - // console.log("message has text " + messageJSON.text); - elements.message['text'] = document.createElement('div'); - if(messageJSON.source == 'sy') { - elements.message.text.setAttribute('class', 'msgTextSystem'); - } - else if(messageJSON.hasOwnProperty('pm') && messageJSON.pm == true) { - elements.message.text.setAttribute('class', 'msgTextPriv'); - } - else if(messageJSON.hasOwnProperty('mention') && messageJSON.mention == true){ - elements.message.text.setAttribute('class', 'msgTextMention'); - } - else { - elements.message.text.setAttribute('class', 'msgText'); - } - - if(messageJSON.source == 'tw') { - messageJSON.text = htmlifyTwitchEmoticons(escapeHtml(twitch_processEmoticons(messageJSON.text, messageJSON.emotes))); - if(messageJSON.hasOwnProperty('bttv_emotes')) { - messageJSON.text = htmlifyBTTVEmoticons(messageJSON.text, messageJSON.bttv_emotes); - } - } - else if(messageJSON.source == 'gg') { - messageJSON.text = htmlifyGGEmoticons(messageJSON.text, messageJSON.emotes) - } - else if(messageJSON.source == 'fs') { - messageJSON.text = htmlifyGGEmoticons(escapeHtml(messageJSON.text), messageJSON.emotes) - } - - // elements.message.text.appendChild(document.createTextNode(messageJSON.text)); - elements.message.text.innerHTML = messageJSON.text; - - elements.message.appendChild(elements.message.text); - - } - document.getElementById('ChatContainer').appendChild(elements.message); - updateMessages(); -} - -function runCommand(message) { - if(message.command == 'reload'){ - window.location.reload(); - } -} \ No newline at end of file diff --git a/http/czt_timed/levels.xml b/http/czt_timed/levels.xml deleted file mode 100644 index 5ae2fbf..0000000 --- a/http/czt_timed/levels.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/http/czt/img/sources/cybergame.png b/img/cybergame.png similarity index 100% rename from http/czt/img/sources/cybergame.png rename to img/cybergame.png diff --git a/http/czt/img/sources/empire.png b/img/empire.png similarity index 100% rename from http/czt/img/sources/empire.png rename to img/empire.png diff --git a/img/fs.png b/img/fs.png new file mode 100644 index 0000000..bf7650f Binary files /dev/null and b/img/fs.png differ diff --git a/http/czt/img/sources/gamerstv.png b/img/gamerstv.png similarity index 100% rename from http/czt/img/sources/gamerstv.png rename to img/gamerstv.png diff --git a/http/czt/img/sources/gg.png b/img/gg.png similarity index 100% rename from http/czt/img/sources/gg.png rename to img/gg.png diff --git a/http/czt/img/sources/gipsyteam.png b/img/gipsyteam.png similarity index 100% rename from http/czt/img/sources/gipsyteam.png rename to img/gipsyteam.png diff --git a/http/czt/img/sources/gohatv.png b/img/gohatv.png similarity index 100% rename from http/czt/img/sources/gohatv.png rename to img/gohatv.png diff --git a/http/czt/img/sources/hitboxtv.png b/img/hitboxtv.png similarity index 100% rename from http/czt/img/sources/hitboxtv.png rename to img/hitboxtv.png diff --git a/http/czt/img/sources/lalka_cup.png b/img/lalka_cup.png similarity index 100% rename from http/czt/img/sources/lalka_cup.png rename to img/lalka_cup.png diff --git a/http/czt/img/sources/midlane.png b/img/midlane.png similarity index 100% rename from http/czt/img/sources/midlane.png rename to img/midlane.png diff --git a/http/czt/img/sources/streamcube.png b/img/streamcube.png similarity index 100% rename from http/czt/img/sources/streamcube.png rename to img/streamcube.png diff --git a/http/czt/img/sources/tw.png b/img/tw.png similarity index 100% rename from http/czt/img/sources/tw.png rename to img/tw.png diff --git a/http/czt/img/sources/youtube.png b/img/youtube.png similarity index 100% rename from http/czt/img/sources/youtube.png rename to img/youtube.png diff --git a/main.py b/main.py index 0c9152c..3341a8d 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import os import imp import Queue @@ -12,10 +13,11 @@ import semantic_version import locale from collections import OrderedDict -from modules.helper.parser import self_heal +from modules.helper.parser import load_from_config_file from modules.helper.system import load_translations_keys +from modules.helper.module import BaseModule -VERSION = '0.3.1' +VERSION = '0.3.5' SEM_VERSION = semantic_version.Version(VERSION) if hasattr(sys, 'frozen'): PYTHON_FOLDER = os.path.dirname(sys.executable) @@ -43,7 +45,7 @@ def get_update(): github_url = "https://api.github.com/repos/DeForce/LalkaChat/releases" try: - update_json = requests.get(github_url) + update_json = requests.get(github_url, timeout=1) if update_json.status_code == 200: update = False update_url = None @@ -65,6 +67,9 @@ def get_language(): def init(): def close(): + for l_module, l_module_dict in loaded_modules.iteritems(): + l_module_dict['class'].apply_settings(system_exit=True) + if window: window.gui.on_close('Closing Program from console') else: @@ -100,12 +105,14 @@ def close(): main_config_dict = OrderedDict() main_config_dict['gui_information'] = OrderedDict() main_config_dict['gui_information']['category'] = 'main' - main_config_dict['gui_information']['width'] = 450 - main_config_dict['gui_information']['height'] = 500 + main_config_dict['gui_information']['width'] = '450' + main_config_dict['gui_information']['height'] = '500' main_config_dict['gui'] = OrderedDict() main_config_dict['gui']['show_hidden'] = False main_config_dict['gui']['gui'] = True main_config_dict['gui']['on_top'] = True + main_config_dict['gui']['show_browser'] = True + main_config_dict['gui']['show_counters'] = True main_config_dict['gui']['reload'] = None main_config_dict['language'] = get_language() @@ -117,23 +124,29 @@ def close(): }, 'non_dynamic': ['language.list_box', 'gui.*'] } - config = self_heal(MAIN_CONF_FILE, main_config_dict) + config = load_from_config_file(MAIN_CONF_FILE, main_config_dict) # Adding config for main module - loaded_modules['main'] = {'folder': CONF_FOLDER, - 'file': main_config['main_conf_file_loc'], - 'filename': main_config['main_conf_file_name'], - 'parser': config, - 'root_folder': main_config['root_folder'], - 'logs_folder': LOG_FOLDER, - 'config': main_config_dict, - 'gui': main_config_gui} + main_class = BaseModule( + conf_params={ + 'folder': CONF_FOLDER, + 'file': main_config['main_conf_file_loc'], + 'filename': main_config['main_conf_file_name'], + 'parser': config, + 'root_folder': main_config['root_folder'], + 'logs_folder': LOG_FOLDER, + 'config': main_config_dict, + 'gui': main_config_gui + } + ) + loaded_modules['main'] = main_class.conf_params() gui_settings['gui'] = main_config_dict[GUI_TAG].get('gui') gui_settings['on_top'] = main_config_dict[GUI_TAG].get('on_top') gui_settings['language'] = main_config_dict.get('language') gui_settings['show_hidden'] = main_config_dict[GUI_TAG].get('show_hidden') - gui_settings['size'] = (main_config_dict['gui_information'].get('width'), - main_config_dict['gui_information'].get('height')) + gui_settings['size'] = (int(main_config_dict['gui_information'].get('width')), + int(main_config_dict['gui_information'].get('height'))) + gui_settings['show_browser'] = main_config_dict['gui'].get('show_browser') # Checking updates log.info("Checking for updates") @@ -155,7 +168,6 @@ def close(): log.info("Loading Chats") # Trying to dynamically load chats that are in config file. chat_modules = os.path.join(CONF_FOLDER, "chat_modules.cfg") - chat_tag = "chats" chat_location = os.path.join(MODULE_FOLDER, "chat") chat_conf_dict = OrderedDict() chat_conf_dict['gui_information'] = {'category': 'chat'} @@ -168,14 +180,20 @@ def close(): 'check': os.path.sep.join(['modules', 'chat']), 'file_extension': False}, 'non_dynamic': ['chats.list_box']} - chat_config = self_heal(chat_modules, chat_conf_dict) - loaded_modules['chat'] = {'folder': CONF_FOLDER, 'file': chat_modules, - 'filename': ''.join(os.path.basename(chat_modules).split('.')[:-1]), - 'parser': chat_config, - 'config': chat_conf_dict, - 'gui': chat_conf_gui} - - for module, settings in chat_config.items(chat_tag): + chat_config = load_from_config_file(chat_modules, chat_conf_dict) + + chat_module = BaseModule( + conf_params={ + 'folder': CONF_FOLDER, 'file': chat_modules, + 'filename': ''.join(os.path.basename(chat_modules).split('.')[:-1]), + 'parser': chat_config, + 'config': chat_conf_dict, + 'gui': chat_conf_gui + } + ) + loaded_modules['chat'] = chat_module.conf_params() + + for module, settings in chat_conf_dict['chats'].iteritems(): log.info("Loading chat module: {0}".format(module)) module_location = os.path.join(chat_location, module + ".py") if os.path.isfile(module_location): @@ -187,7 +205,9 @@ def close(): tmp = imp.load_source(module, module_location) chat_init = getattr(tmp, module) - class_module = chat_init(queue, PYTHON_FOLDER) + class_module = chat_init(queue, PYTHON_FOLDER, + conf_folder=CONF_FOLDER, + conf_file=os.path.join(CONF_FOLDER, '{0}.cfg'.format(module))) loaded_modules[module] = class_module.conf_params() else: log.error("Unable to find {0} module") @@ -208,6 +228,7 @@ def close(): main_config=loaded_modules['main'], loaded_modules=loaded_modules, queue=queue) + loaded_modules['gui'] = window.conf_params() window.start() try: while True: diff --git a/messaging.py b/messaging.py index f1cc88b..c3c0dfb 100644 --- a/messaging.py +++ b/messaging.py @@ -1,5 +1,6 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import os import threading import imp @@ -7,8 +8,11 @@ import logging from collections import OrderedDict +from main import CONF_FOLDER +from modules.helper.module import BaseModule from modules.helper.system import ModuleLoadException, THREADS -from modules.helper.parser import self_heal +from modules.helper.parser import load_from_config_file + log = logging.getLogger('messaging') MODULE_PRI_DEFAULT = '100' @@ -52,17 +56,21 @@ def load_modules(self, main_config, settings): 'view': 'choose_multiple', 'description': True}, 'non_dynamic': ['messaging.*']} - config = self_heal(conf_file, conf_dict) - modules_list['messaging'] = {'folder': main_config['conf_folder'], 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui} + config = load_from_config_file(conf_file, conf_dict) + messaging_module = BaseModule( + conf_params={ + 'folder': main_config['conf_folder'], 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui}) + + modules_list['messaging'] = messaging_module.conf_params() modules = {} # Loading modules from cfg. - if config.items("messaging") > 0: - for module, item in config.items("messaging"): + if len(conf_dict['messaging']) > 0: + for module, item in conf_dict['messaging'].iteritems(): log.info("Loading %s" % module) # We load the module, and then we initalize it. # When writing your modules you should have class with the @@ -73,8 +81,10 @@ def load_modules(self, main_config, settings): try: tmp = imp.load_source(module, file_path) class_init = getattr(tmp, module) - class_module = class_init(main_config['conf_folder'], root_folder=main_config['root_folder'], - main_settings=settings) + class_module = class_init(main_config['conf_folder'], + root_folder=main_config['root_folder'], + main_settings=settings, + conf_file=os.path.join(CONF_FOLDER, '{0}.cfg'.format(module))) params = class_module.conf_params() if 'id' in params: diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 270aa0d..715e9b1 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -1,3 +1,6 @@ +# This Python file uses the following encoding: utf-8 +# -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import json import threading import os @@ -5,28 +8,33 @@ import Queue import re import logging -import time from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.system import system_message -from modules.helper.modules import ChatModule +from modules.helper.parser import load_from_config_file +from modules.helper.system import system_message, translate_key, remove_message_by_id, EMOTE_FORMAT, NA_MESSAGE +from modules.helper.module import ChatModule from ws4py.client.threadedclient import WebSocketClient +from gui import MODULE_KEY logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('goodgame') SOURCE = 'gg' SOURCE_ICON = 'http://goodgame.ru/images/icons/favicon.png' +FILE_ICON = os.path.join('img', 'gg.png') SYSTEM_USER = 'GoodGame' +ID_PREFIX = 'gg_{0}' CONF_DICT = OrderedDict() CONF_DICT['gui_information'] = {'category': 'chat'} CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['show_pm'] = True CONF_DICT['config']['channel_name'] = 'CHANGE_ME' CONF_DICT['config']['socket'] = 'ws://chat.goodgame.ru:8081/chat/websocket' CONF_GUI = { 'config': { - 'hidden': ['socket']}, - 'non_dynamic': ['config.*']} + 'hidden': ['socket'] + }, + 'non_dynamic': ['config.*'], + 'icon': FILE_ICON} class GoodgameMessageHandler(threading.Thread): @@ -41,65 +49,139 @@ def __init__(self, ws_class, **kwargs): self.nick = kwargs.get('nick') self.smiles = kwargs.get('smiles') self.smile_regex = ':(\w+|\d+):' + self.chat_module = kwargs.get('chat_module') + self.kwargs = kwargs def run(self): while True: self.process_message(self.gg_queue.get()) def process_message(self, msg): - if msg['type'] == "message": - # Getting all needed data from received message - # and sending it to queue for further message handling - comp = {'source': self.source, - 'source_icon': SOURCE_ICON, - 'user': msg['data']['user_name'], - 'text': msg['data']['text'], - 'emotes': []} - - smiles_array = re.findall(self.smile_regex, comp['text']) - for smile in smiles_array: - if smile in self.smiles: - smile_info = self.smiles.get(smile) - allow = False - gif = False - if msg['data']['user_rights'] >= 40: - allow = True - elif msg['data']['user_rights'] >= 20 \ - and (smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603'): - allow = True - elif smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603': - if not smile_info['is_premium']: - if smile_info['donate_lvl'] == 0: - allow = True - elif smile_info['donate_lvl'] <= int(msg['data']['payments']): - allow = True - else: - if msg['data']['premium']: - allow = True - - for premium in msg['data']['premiums']: - if smile_info['channel_id'] == str(premium): - if smile_info['is_premium']: - allow = True - gif = True - - if allow: - if smile not in comp['emotes']: - if gif and smile_info['urls']['gif']: - comp['emotes'].append({'emote_id': smile, 'emote_url': smile_info['urls']['gif']}) - else: - comp['emotes'].append({'emote_id': smile, 'emote_url': smile_info['urls']['big']}) - - if re.match('^{0},'.format(self.nick).lower(), comp['text'].lower()): + message_type = msg['type'] + if message_type == "message": + self._process_message(msg) + elif message_type == 'success_join': + self._process_join() + elif message_type == 'error': + self._process_error(msg) + elif message_type == 'user_warn': + self._process_user_warn(msg) + elif message_type == 'remove_message': + self._process_remove_message(msg) + elif message_type == 'user_ban': + self._process_user_ban(msg) + elif message_type == 'channel_counters': + self._process_channel_counters() + + def _process_message(self, msg): + # Getting all needed data from received message + # and sending it to queue for further message handling + comp = {'id': ID_PREFIX.format(msg['data']['message_id']), + 'source': self.source, + 'source_icon': SOURCE_ICON, + 'user': msg['data']['user_name'], + 'text': msg['data']['text'], + 'emotes': {}, + 'type': 'message'} + + self._process_smiles(comp, msg) + + if re.match('^{0},'.format(self.nick).lower(), comp['text'].lower()): + if self.chat_module.conf_params()['config']['config'].get('show_pm'): comp['pm'] = True - self.message_queue.put(comp) - elif msg['type'] == 'success_join': - self.ws_class.system_message('Successfully joined channel {0}'.format(self.nick)) - elif msg['type'] == 'error': - log.info("Received error message: {0}".format(msg)) - if msg['data']['errorMsg'] == 'Invalid channel id': - self.ws_class.close(reason='INV_CH_ID') - log.error("Failed to find channel on GoodGame, please check channel name") + self._send_message(comp) + + def _process_join(self): + self.ws_class.system_message(translate_key(MODULE_KEY.join(['goodgame', 'join_success'])).format(self.nick), + category='connection') + + def _process_error(self, msg): + log.info("Received error message: {0}".format(msg)) + if msg['data']['errorMsg'] == 'Invalid channel id': + self.ws_class.close(reason='INV_CH_ID') + log.error("Failed to find channel on GoodGame, please check channel name") + + def _process_user_warn(self, msg): + self.ws_class.system_message(translate_key(MODULE_KEY.join(['goodgame', 'warning'])).format( + msg['data']['moder_name'], msg['data']['user_name']), category='chat') + + def _process_remove_message(self, msg): + remove_id = ID_PREFIX.format(msg['data']['message_id']) + self.message_queue.put(remove_message_by_id([remove_id], text=self.kwargs['settings'].get('remove_text'))) + + def _process_user_ban(self, msg): + if msg['data']['duration']: + self.ws_class.system_message(translate_key(MODULE_KEY.join(['goodgame', 'ban'])).format( + msg['data']['moder_name'], + msg['data']['user_name'], + msg['data']['duration']/60, + msg['data']['reason']), + category='chat') + else: + if msg['data']['permanent']: + self.ws_class.system_message( + translate_key(MODULE_KEY.join(['goodgame', 'ban_permanent'])).format(msg['data']['moder_name'], + msg['data']['user_name']), + category='chat' + ) + else: + self.ws_class.system_message(translate_key(MODULE_KEY.join(['goodgame', 'unban'])).format( + msg['data']['moder_name'], + msg['data']['user_name']), category='chat') + + def _process_channel_counters(self): + try: + self.chat_module.set_viewers(self.chat_module.get_viewers()) + except Exception as exc: + log.exception(exc) + + def _send_message(self, comp): + self._post_process_emotes(comp) + self.message_queue.put(comp) + + @staticmethod + def _post_process_emotes(comp): + comp['text'] = re.sub(':(\w+|\d+):', EMOTE_FORMAT.format('\\1'), comp['text']) + + def _process_smiles(self, comp, msg): + smiles_array = re.findall(self.smile_regex, comp['text']) + for smile in smiles_array: + if smile in self.smiles: + smile_info = self.smiles.get(smile) + allow = False + gif = False + if msg['data']['user_rights'] >= 40: + allow = True + elif msg['data']['user_rights'] >= 20 \ + and (smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603'): + allow = True + elif smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603': + if not smile_info['is_premium']: + if smile_info['donate_lvl'] == 0: + allow = True + elif smile_info['donate_lvl'] <= int(msg['data']['payments']): + allow = True + else: + if msg['data']['premium']: + allow = True + + for premium in msg['data']['premiums']: + if smile_info['channel_id'] == str(premium): + if smile_info['is_premium']: + allow = True + gif = True + + if allow: + if smile not in comp['emotes']: + if gif and smile_info['urls']['gif']: + comp['emotes'][smile] = {'emote_url': smile_info['urls']['gif']} + else: + comp['emotes'][smile] = {'emote_url': smile_info['urls']['big']} + emotes_list = [] + for emote, data in comp['emotes'].iteritems(): + emotes_list.append({'emote_id': emote, + 'emote_url': data['emote_url']}) + comp['emotes'] = emotes_list class GGChat(WebSocketClient): @@ -112,6 +194,7 @@ def __init__(self, ws, **kwargs): self.gg_queue = Queue.Queue() self.main_thread = kwargs.get('main_thread') + self.chat_module = kwargs.get('chat_module') self.crit_error = False message_handler = GoodgameMessageHandler(self, gg_queue=self.gg_queue, **kwargs) @@ -120,19 +203,25 @@ def __init__(self, ws, **kwargs): def opened(self): success_msg = "Connection Successful" log.info(success_msg) - self.system_message(success_msg) + self.chat_module.set_online() + try: + self.chat_module.set_viewers(self.chat_module.get_viewers()) + except Exception as exc: + log.exception(exc) + self.system_message(translate_key(MODULE_KEY.join(['goodgame', 'connection_success'])), category='connection') # Sending join channel command to goodgame websocket join = json.dumps({'type': "join", 'data': {'channel_id': self.ch_id, 'hidden': "true"}}, sort_keys=False) self.send(join) # self.ggPing() log.info("Sent join message") - + def closed(self, code, reason=None): log.info("Connection Closed Down") + self.chat_module.set_offline() if 'INV_CH_ID' in reason: self.crit_error = True else: - self.system_message("Connection died, trying to reconnect") + self.system_message(translate_key(MODULE_KEY.join(['goodgame', 'connection_died'])), category='connection') timer = threading.Timer(5.0, self.main_thread.connect) timer.start() @@ -140,13 +229,13 @@ def received_message(self, mes): # Deserialize message to json for easier parsing self.gg_queue.put(json.loads(str(mes))) - def system_message(self, msg): + def system_message(self, msg, category='system'): system_message(msg, self.queue, SOURCE, - icon=SOURCE_ICON, from_user=SYSTEM_USER) + icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) class GGThread(threading.Thread): - def __init__(self, queue, address, nick): + def __init__(self, queue, address, nick, **kwargs): threading.Thread.__init__(self) # Basic value setting. # Daemon is needed so when main programm exits @@ -156,7 +245,7 @@ def __init__(self, queue, address, nick): self.address = address self.nick = nick self.ch_id = None - self.kwargs = {} + self.kwargs = kwargs def load_config(self): try: @@ -193,7 +282,7 @@ def load_config(self): log.warning("Unable to get channel name, error {0}\nArgs: {1}".format(exc.message, exc.args)) return True - + def run(self): self.connect() @@ -223,17 +312,52 @@ def __init__(self, queue, python_folder, **kwargs): log.info("Initializing goodgame chat") conf_file = os.path.join(conf_folder, "goodgame.cfg") - config = self_heal(conf_file, CONF_DICT) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI} + config = load_from_config_file(conf_file, CONF_DICT) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': CONF_DICT, + 'gui': CONF_GUI, + 'settings': {}}) + self.queue = queue self.host = CONF_DICT['config']['socket'] self.channel_name = CONF_DICT['config']['channel_name'] + self.gg = None def load_module(self, *args, **kwargs): + ChatModule.load_module(self, *args, **kwargs) + if 'webchat' in self._loaded_modules: + self._loaded_modules['webchat']['class'].add_depend('goodgame') + self._conf_params['settings']['remove_text'] = self.get_remove_text() # Creating new thread with queue in place for messaging transfers - gg = GGThread(self.queue, self.host, self.channel_name) + gg = GGThread(self.queue, self.host, self.channel_name, + settings=self._conf_params['settings'], chat_module=self) + self.gg = gg gg.start() + + def get_remove_text(self): + if self._loaded_modules['webchat']['style_settings']['keys'].get('remove_message'): + return self._loaded_modules['webchat']['style_settings']['keys'].get('remove_text') + return None + + def get_viewers(self): + streams_url = 'http://api2.goodgame.ru/streams/{0}'.format(self.gg.ch_id) + try: + request = requests.get(streams_url) + if request.status_code == 200: + json_data = request.json() + if json_data['status'] == 'Live': + return request.json().get('player_viewers') + else: + return NA_MESSAGE + else: + raise Exception("Not successful status code: {0}".format(request.status_code)) + except Exception as exc: + log.warning("Unable to get user count, error {0}\nArgs: {1}".format(exc.message, exc.args)) + + def apply_settings(self, **kwargs): + ChatModule.apply_settings(self, **kwargs) + if 'webchat' in kwargs.get('from_depend', []): + self._conf_params['settings']['remove_text'] = self.get_remove_text() diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index 0d75f76..5e555d6 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -1,3 +1,4 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov import json import threading import time @@ -7,28 +8,32 @@ import logging from collections import OrderedDict from ws4py.client.threadedclient import WebSocketClient -from modules.helper.modules import ChatModule -from modules.helper.parser import self_heal -from modules.helper.system import system_message +from modules.helper.module import ChatModule +from modules.helper.parser import load_from_config_file +from modules.helper.system import system_message, translate_key, EMOTE_FORMAT +from gui import MODULE_KEY logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('sc2tv') SOURCE = 'fs' SOURCE_ICON = 'http://funstream.tv/build/images/icon_home.png' +FILE_ICON = os.path.join('img', 'fs.png') SYSTEM_USER = 'Funstream' -PING_DELAY = 30 +PING_DELAY = 10 CONF_DICT = OrderedDict() CONF_DICT['gui_information'] = {'category': 'chat'} CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['show_pm'] = True CONF_DICT['config']['channel_name'] = 'CHANGE_ME' CONF_DICT['config']['socket'] = 'ws://funstream.tv/socket.io/' CONF_GUI = { 'config': { 'hidden': ['socket']}, - 'non_dynamic': ['config.*']} + 'non_dynamic': ['config.*'], + 'icon': FILE_ICON} class FsChat(WebSocketClient): @@ -39,6 +44,7 @@ def __init__(self, ws, queue, channel_name, **kwargs): self.queue = queue self.channel_name = channel_name self.main_thread = kwargs.get('main_thread') # type: FsThread + self.chat_module = kwargs.get('chat_module') self.crit_error = False self.channel_id = self.fs_get_id() @@ -46,39 +52,28 @@ def __init__(self, ws, queue, channel_name, **kwargs): self.smiles = kwargs.get('smiles') self.smile_regex = ':(\w+|\d+):' - # Because funstream API is fun, we have to iterate the - # requests in "proper" format: - # - # 42Iterator["command",{"params":"param"}] - # ex: 420["/chat/join",{'channel':"stream/30000"} - # ex: 421["/chat/join",{'channel':"stream/30000"} - # ex: 429["/chat/join",{'channel':"stream/30000"} - # ex: 4210["/chat/join",{'channel':"stream/30000"} - # - # Also, funstream API send duplicates of the messages - # so we have to ignore the duplicates. - # We are doing so by creating special array which has - # last N buffer of unique ID's self.iter = 0 self.duplicates = [] self.users = [] + self.request_array = [] self.bufferForDup = 20 def opened(self): log.info("Websocket Connection Succesfull") - self.fs_system_message("Connected") + self.fs_system_message(translate_key(MODULE_KEY.join(['sc2tv', 'connection_success'])), category='connection') def closed(self, code, reason=None): + self.chat_module.set_offline() if reason == 'INV_CH_ID': self.crit_error = True else: log.info("Websocket Connection Closed Down") - self.fs_system_message("Connection died, trying to reconnect") + self.fs_system_message(translate_key(MODULE_KEY.join(['sc2tv', 'connection_died'])), category='connection') timer = threading.Timer(5.0, self.main_thread.connect) timer.start() - def fs_system_message(self, message): - system_message(message, self.queue, source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER) + def fs_system_message(self, message, category='system'): + system_message(message, self.queue, source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) @staticmethod def allow_smile(smile, subscriptions): @@ -95,65 +90,26 @@ def allow_smile(smile, subscriptions): return allow def received_message(self, mes): - # Funstream send all kind of different messages - # Some of them are strange asnwers like "40". - # For that we are trying to find real messages - # which are more than 5 char length. - # - # Websocket has it's own type, so we serialise it to string. - message = str(mes) - if len(message) > 5: - # "Fun" messages consists of strange format: - # 43Iter{json} - # ex: 430{'somedata': 'somedata'} - # We need to just get the json, so we "regexp" it. - if re.findall('{.*}', message)[0]: - # If message does have JSON (some of them dont, dont know why) - # we analyze the real "json" message. - message = json.loads(re.findall('{.*}', message)[0]) - for dict_item in message: - # SID type is "start" packet, after that we can join channels, - # at least I think so. - if dict_item == 'sid': - # "Funstream" has some interesting infrastructure, so - # we first need to find the channel ID from - # nickname of streamer we need to connect to. - self.fs_join() - self.fs_ping() - elif dict_item == 'status': - self.fs_system_message('Joined channel {0}'.format(self.channel_name)) - elif dict_item == 'id': - try: - self.duplicates.index(message[dict_item]) - except ValueError: - comp = {'source': self.source, - 'source_icon': SOURCE_ICON, - 'user': message['from']['name'], - 'text': message['text'], - 'emotes': []} - if message['to'] is not None: - comp['to'] = message['to']['name'] - if comp['to'] == self.channel_name: - comp['pm'] = True - else: - comp['to'] = None - - smiles_array = re.findall(self.smile_regex, comp['text']) - for smile in smiles_array: - for smile_find in self.smiles: - if smile_find['code'] == smile: - if self.allow_smile(smile_find, message['store']['subscriptions']): - comp['emotes'].append({'emote_id': smile, 'emote_url': smile_find['url']}) - - self.queue.put(comp) - self.duplicates.append(message[dict_item]) - if len(self.duplicates) > self.bufferForDup: - self.duplicates.pop(0) + if mes.data == '40': + return + if mes.data in ['2', '3']: + return + regex = re.match('(\d+)(.*)', mes.data) + sio_iter, json_message = regex.groups() + if sio_iter == '0': + self._process_welcome() + elif sio_iter[:2] in '42': + self._process_websocket_event(json.loads(json_message)) + elif sio_iter[:2] in '43': + self._process_websocket_ack(sio_iter[2:], json.loads(json_message)) def fs_get_id(self): # We get ID from POST request to funstream API, and it hopefuly # answers us the correct ID of the channel we need to connect to - payload = "{'id': null, 'name': \"" + self.channel_name + "\"}" + payload = { + 'id': None, + 'name': self.channel_name + } try: request = requests.post("http://funstream.tv/api/user", data=payload, timeout=5) if request.status_code == 200: @@ -172,30 +128,110 @@ def fs_get_id(self): return None def fs_join(self): - # Because we need to iterate each message we iterate it! - iter_sio = "42"+str(self.iter) - self.iter += 1 - # Then we send the message acording to needed format and # hope it joins us if self.channel_id: - join = str(iter_sio) + "[\"/chat/join\", " + json.dumps({'channel': "stream/" + str(self.channel_id)}, - sort_keys=False) + "]" - self.send(join) - self.fs_system_message("Joining channel {0}".format(self.channel_name)) - log.info("Joined channel {0}".format(self.channel_id)) + payload = [ + '/chat/join', + { + 'channel': 'stream/{0}'.format(str(self.channel_id)) + } + ] + self.fs_send(payload) + + msg_joining = translate_key(MODULE_KEY.join(['sc2tv', 'joining'])) + self.fs_system_message(msg_joining.format(self.channel_name), category='connection') + log.info(msg_joining.format(self.channel_id)) + + def fs_send(self, payload): + iter_sio = "42"+str(self.iter) + + self.send('{iter}{payload}'.format(iter=iter_sio, + payload=json.dumps(payload))) + history_item = { + 'iter': str(self.iter), + 'payload': payload + } + self.iter += 1 + if len(self.request_array) > 20: + del self.request_array[0] + self.request_array.append(history_item) def fs_ping(self): - # Because funstream is not your normal websocket they - # have own "ping/pong" algorithm, and WE have to send ping. - # Yes, I don't know why. - # We have to send ping message every 30 seconds, or funstream will - # disconnect us. So we have to create separate thread for it. - # Dont understand why server is not sending his own pings, it - # would be sooooo easier. ping_thread = FsPingThread(self) ping_thread.start() + def _process_websocket_event(self, message): + event_from, event_dict = message + if event_from == '/chat/message': + self._process_message(event_dict) + + def _process_websocket_ack(self, sio_id, message): + if isinstance(message, list): + if len(message) == 1: + message = message[0] + for item in self.request_array: # type: dict + if item['iter'] == sio_id: + item_path = item['payload'][0] + self._process_answer(item_path, message) + break + + def _process_welcome(self): + self.fs_join() + self.fs_ping() + + def _process_answer(self, path, message): + if path == '/chat/join': + self._process_joined() + elif path == '/chat/channel/list': + self._process_channel_list(message) + + def _process_message(self, message): + try: + self.duplicates.index(message['id']) + except ValueError: + comp = {'source': self.source, + 'source_icon': SOURCE_ICON, + 'user': message['from']['name'], + 'text': message['text'], + 'emotes': [], + 'type': 'message'} + if message['to'] is not None: + comp['to'] = message['to']['name'] + if comp['to'] == self.channel_name: + if self.chat_module.conf_params()['config']['config'].get('show_pm'): + comp['pm'] = True + else: + comp['to'] = None + + smiles_array = re.findall(self.smile_regex, comp['text']) + for smile in smiles_array: + for smile_find in self.smiles: + if smile_find['code'] == smile: + if self.allow_smile(smile_find, message['store']['subscriptions']): + comp['emotes'].append({'emote_id': smile, 'emote_url': smile_find['url']}) + + self.duplicates.append(message['id']) + if len(self.duplicates) > self.bufferForDup: + self.duplicates.pop(0) + self._send_message(comp) + + def _process_joined(self): + self.chat_module.set_online() + self.fs_system_message( + translate_key(MODULE_KEY.join(['sc2tv', 'join_success'])).format(self.channel_name), category='connection') + + def _process_channel_list(self, message): + self.chat_module.set_viewers(message['result']['amount']) + + def _send_message(self, comp): + self._post_process_emotes(comp) + self.queue.put(comp) + + @staticmethod + def _post_process_emotes(comp): + comp['text'] = re.sub(':(\w+|\d+):', EMOTE_FORMAT.format('\\1'), comp['text']) + class FsPingThread(threading.Thread): def __init__(self, ws): @@ -207,11 +243,12 @@ def __init__(self, ws): def run(self): while not self.ws.terminated: self.ws.send("2") + self.ws.chat_module.get_viewers(self.ws) time.sleep(PING_DELAY) class FsThread(threading.Thread): - def __init__(self, queue, socket, channel_name): + def __init__(self, queue, socket, channel_name, **kwargs): threading.Thread.__init__(self) # Basic value setting. # Daemon is needed so when main programm exits @@ -221,6 +258,8 @@ def __init__(self, queue, socket, channel_name): self.socket = socket self.channel_name = channel_name self.smiles = [] + self.ws = None + self.kwargs = kwargs def run(self): self.connect() @@ -240,14 +279,14 @@ def connect(self): self.smiles.append(smile) except requests.ConnectionError: log.error("Unable to get smiles") - ws = FsChat(self.socket, self.queue, self.channel_name, protocols=['websocket'], smiles=self.smiles, - main_thread=self) - if ws.crit_error: + self.ws = FsChat(self.socket, self.queue, self.channel_name, protocols=['websocket'], smiles=self.smiles, + main_thread=self, **self.kwargs) + if self.ws.crit_error: log.critical("Got critical error, halting") break - elif ws.channel_id and self.smiles: - ws.connect() - ws.run_forever() + elif self.ws.channel_id and self.smiles: + self.ws.connect() + self.ws.run_forever() break @@ -259,17 +298,45 @@ def __init__(self, queue, python_folder, **kwargs): # Reading config from main directory. conf_folder = os.path.join(python_folder, "conf") conf_file = os.path.join(conf_folder, "sc2tv.cfg") - config = self_heal(conf_file, CONF_DICT) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI} + config = load_from_config_file(conf_file, CONF_DICT) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': CONF_DICT, + 'gui': CONF_GUI}) + self.queue = queue self.socket = CONF_DICT['config']['socket'] self.channel_name = CONF_DICT['config']['channel_name'] + self.fs_thread = None def load_module(self, *args, **kwargs): + ChatModule.load_module(self, *args, **kwargs) # Creating new thread with queue in place for messaging transfers - fs = FsThread(self.queue, self.socket, self.channel_name) + fs = FsThread(self.queue, self.socket, self.channel_name, chat_module=self) + self.fs_thread = fs fs.start() + + def get_viewers(self, ws): + user_data = {'name': ws.channel_name} + status_data = {'slug': ws.channel_name} + request = ['/chat/channel/list', {'channel': 'stream/{0}'.format(str(ws.channel_id))}] + + try: + user_request = requests.post('http://funstream.tv/api/user', timeout=5, data=user_data) + if user_request.status_code == 200: + status_data['slug'] = user_request.json()['slug'] + except requests.ConnectionError: + log.error("Unable to get smiles") + + try: + status_request = requests.post('http://funstream.tv/api/stream', timeout=5, data=status_data) + if status_request.status_code == 200: + if status_request.json()['online']: + ws.fs_send(request) + else: + self.set_viewers('N/A') + + except requests.ConnectionError: + log.error("Unable to get smiles") diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 90eca23..aec1b67 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -1,4 +1,4 @@ - +# Copyright (C) 2016 CzT/Vladislav Ivanov import irc.client import threading import os @@ -9,9 +9,10 @@ import Queue from collections import OrderedDict import time -from modules.helper.parser import self_heal -from modules.helper.modules import ChatModule -from modules.helper.system import system_message +from modules.helper.parser import load_from_config_file +from modules.helper.module import ChatModule +from modules.helper.system import system_message, translate_key, remove_message_by_user, EMOTE_FORMAT, NA_MESSAGE +from gui import MODULE_KEY logging.getLogger('irc').setLevel(logging.ERROR) logging.getLogger('requests').setLevel(logging.ERROR) @@ -20,16 +21,19 @@ emote_bits_theme = 'dark' emote_bits_type = 'static' emote_bits_url = 'static-cdn.jtvnw.net/bits/{theme}/{type}/{color}/{size}' +emote_smile_url = 'http://static-cdn.jtvnw.net/emoticons/v1/{id}/1.0' NOT_FOUND = 'none' SOURCE = 'tw' SOURCE_ICON = 'https://www.twitch.tv/favicon.ico' +FILE_ICON = os.path.join('img', 'tw.png') SYSTEM_USER = 'Twitch.TV' -PING_DELAY = 30 +PING_DELAY = 10 CONF_DICT = OrderedDict() CONF_DICT['gui_information'] = {'category': 'chat'} CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['show_pm'] = True CONF_DICT['config']['channel'] = 'CHANGE_ME' CONF_DICT['config']['bttv'] = True CONF_DICT['config']['host'] = 'irc.twitch.tv' @@ -37,7 +41,8 @@ CONF_GUI = { 'config': { 'hidden': ['host', 'port']}, - 'non_dynamic': ['config.*']} + 'non_dynamic': ['config.*'], + 'icon': FILE_ICON} class TwitchUserError(Exception): @@ -52,10 +57,13 @@ def __init__(self, queue, twitch_queue, **kwargs): self.twitch_queue = twitch_queue self.source = SOURCE + self.irc_class = kwargs.get('irc_class') # type: IRC self.nick = kwargs.get('nick') - self.bttv = kwargs.get('bttv') + self.bttv = kwargs.get('bttv_smiles_dict', {}) self.badges = kwargs.get('badges') self.custom_badges = kwargs.get('custom_badges') + self.chat_module = kwargs.get('chat_module') + self.kwargs = kwargs def run(self): while True: @@ -68,74 +76,154 @@ def process_message(self, msg): # Also, there is slight problem with some users, they don't have # the display-name tag, so we have to check their "real" username # and capitalize it because twitch does so, so we do the same. - comp = {'source': self.source, - 'source_icon': SOURCE_ICON, - 'badges': [], - 'emotes': [], - 'bttv_emotes': [], - 'user': 'TwitchSystem', - 'msg_type': msg.type} + if msg.type in ['pubmsg', 'action']: + self._handle_message(msg) + elif msg.type in ['clearchat']: + self._handle_clearchat(msg) + elif msg.type in ['usernotice']: + self._handle_usernotice(msg) + + def _handle_badges(self, message, badges): + for badge in badges.split(','): + badge_tag, badge_size = badge.split('/') + # Fix some of the names + badge_tag = badge_tag.replace('moderator', 'mod') + + if badge_tag in self.badges: + badge_info = self.badges.get(badge_tag) + if 'svg' in badge_info: + url = badge_info.get('svg') + elif 'image' in badge_info: + url = badge_info.get('image') + else: + url = 'none' + elif badge_tag in self.custom_badges: + badge_info = self.custom_badges.get(badge_tag)['versions'][badge_size] + url = badge_info.get('image_url_4x') + else: + url = NOT_FOUND + message['badges'].append({'badge': badge_tag, 'size': badge_size, 'url': url}) + + @staticmethod + def _handle_display_name(message, name): + message['display_name'] = name if name else message['user'] + + @staticmethod + def _handle_emotes(message, tag_value): + for emote in tag_value.split('/'): + emote_id, emote_pos_diap = emote.split(':') + message['emotes'].append({'emote_id': emote_id, + 'positions': emote_pos_diap.split(','), + 'emote_url': emote_smile_url.format(id=emote_id)}) + + def _handle_bttv_smiles(self, message): + for word in message['text'].split(): + if word in self.bttv: + bttv_smile = self.bttv.get(word) + message['bttv_emotes'][bttv_smile['regex']] = { + 'emote_id': bttv_smile['regex'], + 'emote_url': 'https:{0}'.format(bttv_smile['url']) + } + + def _handle_pm(self, message): + if re.match('^@?{0}[ ,]?'.format(self.nick), message['text'].lower()): + if self.chat_module.conf_params()['config']['config'].get('show_pm'): + message['pm'] = True + + def _handle_clearchat(self, msg): + self.message_queue.put(remove_message_by_user(msg.arguments, + text=self.kwargs['settings'].get('remove_text'))) + + def _handle_usernotice(self, msg): + for tag in msg.tags: + tag_value, tag_key = tag.values() + if tag_key == 'system-msg': + msg_text = tag_value + self.irc_class.system_message(msg_text, category='chat') + break + if msg.arguments: + self._handle_message(msg, sub_message=True) + + def _handle_message(self, msg, sub_message=False): + message = {'source': self.source, + 'source_icon': SOURCE_ICON, + 'badges': [], + 'emotes': [], + 'bttv_emotes': {}, + 'user': msg.source.split('!')[0], + 'type': 'message', + 'msg_type': msg.type} + + if message['user'] == 'twitchnotify': + self.irc_class.system_message(msg.arguments.pop(), category='chat') + return + + message['text'] = msg.arguments.pop() + for tag in msg.tags: tag_value, tag_key = tag.values() if tag_key == 'display-name': - if tag_value: - comp['user'] = tag_value - else: - # If there is not display-name then we strip the user - # from the string and use it as it is. - comp['user'] = msg.source.split('!')[0].capitalize() + self._handle_display_name(message, tag_value) elif tag_key == 'badges' and tag_value: - for badge in tag_value.split(','): - badge_tag, badge_size = badge.split('/') - # Fix some of the names - badge_tag = badge_tag.replace('moderator', 'mod') - - if badge_tag in self.badges: - badge_info = self.badges.get(badge_tag) - if 'svg' in badge_info: - url = badge_info.get('svg') - elif 'image' in badge_info: - url = badge_info.get('image') - else: - url = 'none' - elif badge_tag in self.custom_badges: - badge_info = self.custom_badges.get(badge_tag)['versions'][badge_size] - url = badge_info.get('image_url_4x') - else: - url = NOT_FOUND - comp['badges'].append({'badge': badge_tag, 'size': badge_size, 'url': url}) - + self._handle_badges(message, tag_value) elif tag_key == 'emotes' and tag_value: - emotes_split = tag_value.split('/') - for emote in emotes_split: - emote_name, emote_pos_diap = emote.split(':') - emote_pos_list = emote_pos_diap.split(',') - comp['emotes'].append({'emote_id': emote_name, 'emote_pos': emote_pos_list}) + self._handle_emotes(message, tag_value) - # Then we comp the message and send it to queue for message handling. - comp['text'] = msg.arguments.pop() + self._handle_bttv_smiles(message) + self._handle_pm(message) - for word in comp['text'].split(): - if word in self.bttv: - bttv_smile = self.bttv.get(word) - comp['bttv_emotes'].append({'emote_id': bttv_smile['regex'], - 'emote_url': 'http:{0}'.format(bttv_smile['url'])}) + if sub_message: + self._handle_sub_message(message) - if re.match('^@?{0}[ ,]?'.format(self.nick), comp['text'].lower()): - comp['pm'] = True + self._send_message(message) - self.message_queue.put(comp) + @staticmethod + def _handle_sub_message(message): + message['sub_message'] = True + + def _send_message(self, message): + self._post_process_emotes(message) + self._post_process_bttv_emotes(message) + self.message_queue.put(message) + + @staticmethod + def _post_process_emotes(message): + conveyor_emotes = [] + for emote in message['emotes']: + for position in emote['positions']: + start, end = position.split('-') + conveyor_emotes.append({'emote_id': emote['emote_id'], + 'start': int(start), + 'end': int(end)}) + conveyor_emotes = sorted(conveyor_emotes, key=lambda k: k['start'], reverse=True) + + for emote in conveyor_emotes: + message['text'] = u'{start}{emote}{end}'.format(start=message['text'][:emote['start']], + end=message['text'][emote['end'] + 1:], + emote=EMOTE_FORMAT.format(emote['emote_id'])) + + @staticmethod + def _post_process_bttv_emotes(message): + for emote, data in message['bttv_emotes'].iteritems(): + message['text'] = message['text'].replace(emote, EMOTE_FORMAT.format(emote)) + message['emotes'].append(data) + pass class TwitchPingHandler(threading.Thread): - def __init__(self, irc_connection): + def __init__(self, irc_connection, chat_module): threading.Thread.__init__(self) self.irc_connection = irc_connection + self.chat_module = chat_module def run(self): log.info("Ping started") while self.irc_connection.connected: self.irc_connection.ping("keep-alive") + try: + self.chat_module.set_viewers(self.chat_module.get_viewers()) + except Exception as exc: + log.exception(exc) time.sleep(PING_DELAY) @@ -149,21 +237,22 @@ def __init__(self, queue, channel, **kwargs): self.twitch_queue = Queue.Queue() self.tw_connection = None self.main_class = kwargs.get('main_class') + self.chat_module = kwargs.get('chat_module') - msg_handler = TwitchMessageHandler(queue, self.twitch_queue, - nick=self.nick, - bttv=kwargs.get('bttv_smiles_dict', {}), - badges=kwargs.get('badges', {}), - custom_badges=kwargs.get('custom_badges', {})) - msg_handler.start() + self.msg_handler = TwitchMessageHandler(queue, self.twitch_queue, + irc_class=self, + nick=self.nick, + **kwargs) + self.msg_handler.start() - def system_message(self, message): + def system_message(self, message, category='system'): system_message(message, self.queue, - source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER) + source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) def on_disconnect(self, connection, event): log.info("Connection lost") - self.system_message("Connection died, trying to reconnect") + self.chat_module.set_offline() + self.system_message(translate_key(MODULE_KEY.join(['twitch', 'connection_died'])), category='connection') timer = threading.Timer(5.0, self.reconnect, args=[self.main_class.host, self.main_class.port, self.main_class.nickname]) timer.start() @@ -182,19 +271,22 @@ def reconnect(self, host, port, nickname): def on_welcome(self, connection, event): log.info("Welcome Received, joining {0} channel".format(self.channel)) self.tw_connection = connection - self.system_message('Joining channel {0}'.format(self.channel)) + self.system_message(translate_key(MODULE_KEY.join(['twitch', 'joining'])).format(self.channel), + category='connection') + self.chat_module.set_online() # After we receive IRC Welcome we send request for join and # request for Capabilities (Twitch color, Display Name, # Subscriber, etc) connection.join(self.channel) connection.cap('REQ', ':twitch.tv/tags') - ping_handler = TwitchPingHandler(connection) + connection.cap('REQ', ':twitch.tv/commands') + ping_handler = TwitchPingHandler(connection, self.chat_module) ping_handler.start() def on_join(self, connection, event): - msg = "Joined {0} channel".format(self.channel) + msg = translate_key(MODULE_KEY.join(['twitch', 'join_success'])).format(self.channel) log.info(msg) - self.system_message(msg) + self.system_message(msg, category='connection') def on_pubmsg(self, connection, event): self.twitch_queue.put(event) @@ -202,9 +294,15 @@ def on_pubmsg(self, connection, event): def on_action(self, connection, event): self.twitch_queue.put(event) + def on_clearchat(self, connection, event): + self.twitch_queue.put(event) + + def on_usernotice(self, connection, event): + self.twitch_queue.put(event) + class twThread(threading.Thread): - def __init__(self, queue, host, port, channel, bttv_smiles, anon=True): + def __init__(self, queue, host, port, channel, bttv_smiles, anon=True, **kwargs): threading.Thread.__init__(self) # Basic value setting. # Daemon is needed so when main programm exits @@ -216,7 +314,8 @@ def __init__(self, queue, host, port, channel, bttv_smiles, anon=True): self.port = port self.channel = channel self.bttv_smiles = bttv_smiles - self.kwargs = {} + self.kwargs = kwargs + self.chat_module = kwargs.get('chat_module') if bttv_smiles: self.kwargs['bttv_smiles_dict'] = {} @@ -240,13 +339,14 @@ def run(self): log.info("Connecting, try {0}".format(try_count)) try: if self.load_config(): - irc_client = IRC(self.queue, self.channel, main_class=self, **self.kwargs) - irc_client.connect(self.host, self.port, self.nickname) - irc_client.start() + irc = IRC(self.queue, self.channel, main_class=self, **self.kwargs) + irc.connect(self.host, self.port, self.nickname) + irc.start() log.info("Connection closed") break except TwitchUserError: log.critical("Unable to find twitch user, please fix") + self.chat_module.set_offline() break except Exception as exc: log.exception(exc) @@ -323,20 +423,50 @@ def __init__(self, queue, python_folder, **kwargs): conf_folder = os.path.join(python_folder, "conf") conf_file = os.path.join(conf_folder, "twitch.cfg") - config = self_heal(conf_file, CONF_DICT) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI} + config = load_from_config_file(conf_file, CONF_DICT) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': CONF_DICT, + 'gui': CONF_GUI, + 'settings': {}}) self.queue = queue self.host = CONF_DICT['config']['host'] - self.port = CONF_DICT['config']['port'] + self.port = int(CONF_DICT['config']['port']) self.channel = CONF_DICT['config']['channel'] self.bttv = CONF_DICT['config']['bttv'] def load_module(self, *args, **kwargs): - # Creating new thread with queue in place for messaging transfers - tw = twThread(self.queue, self.host, self.port, self.channel, self.bttv) + ChatModule.load_module(self, *args, **kwargs) + if 'webchat' in self._loaded_modules: + self._loaded_modules['webchat']['class'].add_depend('twitch') + self._conf_params['settings']['remove_text'] = self.get_remove_text() + tw = twThread(self.queue, self.host, self.port, self.channel, self.bttv, + settings=self._conf_params['settings'], chat_module=self) tw.start() + + def get_viewers(self): + streams_url = 'https://api.twitch.tv/kraken/streams/{0}'.format(self.channel) + try: + request = requests.get(streams_url, headers=headers) + if request.status_code == 200: + json_data = request.json() + if json_data['stream']: + return request.json()['stream'].get('viewers', NA_MESSAGE) + return NA_MESSAGE + else: + raise Exception("Not successful status code: {0}".format(request.status_code)) + except Exception as exc: + log.warning("Unable to get user count, error {0}\nArgs: {1}".format(exc.message, exc.args)) + + def get_remove_text(self): + if self._loaded_modules['webchat']['style_settings']['keys'].get('remove_message'): + return self._loaded_modules['webchat']['style_settings']['keys'].get('remove_text') + return None + + def apply_settings(self, **kwargs): + ChatModule.apply_settings(self, **kwargs) + if 'webchat' in kwargs.get('from_depend', []): + self._conf_params['settings']['remove_text'] = self.get_remove_text() diff --git a/modules/helper/module.py b/modules/helper/module.py new file mode 100644 index 0000000..af7e290 --- /dev/null +++ b/modules/helper/module.py @@ -0,0 +1,70 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov +from parser import save_settings + + +class BaseModule: + def __init__(self, *args, **kwargs): + self._conf_params = kwargs.get('conf_params', {}) + self._loaded_modules = None + self._rest_api = {} + self._module_name = self.__class__.__name__ + + def conf_params(self): + params = self._conf_params + params['class'] = self + return params + + def load_module(self, *args, **kwargs): + self._loaded_modules = kwargs.get('loaded_modules') + + def gui_button_press(self, *args): + pass + + def apply_settings(self, **kwargs): + """ + :param kwargs: + system_exit - param provided if system is exiting + :return: + """ + save_settings(self.conf_params()) + + def rest_api(self): + return self._rest_api + + +class MessagingModule(BaseModule): + def __init__(self, *args, **kwargs): + BaseModule.__init__(self, *args, **kwargs) + self._conf_params['dependencies'] = set() + + def process_message(self, message, queue, **kwargs): + return message + + def add_depend(self, module_name): + self._conf_params['dependencies'].add(module_name) + + def remove_depend(self, module_name): + self._conf_params['dependencies'].discard(module_name) + + +class ChatModule(BaseModule): + def __init__(self, *args, **kwargs): + BaseModule.__init__(self, *args, **kwargs) + + def set_viewers(self, viewers): + if 'gui' in self._loaded_modules: + gui_class = self._loaded_modules['gui']['class'] + if gui_class.gui.status_frame: + gui_class.gui.status_frame.set_viewers(self._module_name, viewers) + + def set_online(self): + if 'gui' in self._loaded_modules: + gui_class = self._loaded_modules['gui']['class'] + if gui_class.gui.status_frame: + gui_class.gui.status_frame.set_online(self._module_name) + + def set_offline(self): + if 'gui' in self._loaded_modules: + gui_class = self._loaded_modules['gui']['class'] + if gui_class.gui.status_frame: + gui_class.gui.status_frame.set_offline(self._module_name) diff --git a/modules/helper/modules.py b/modules/helper/modules.py deleted file mode 100644 index ab02447..0000000 --- a/modules/helper/modules.py +++ /dev/null @@ -1,32 +0,0 @@ - - -class BaseModule: - def __init__(self, *args, **kwargs): - self._conf_params = {} - - def conf_params(self): - params = self._conf_params - params['class'] = self - return params - - def load_module(self, *args, **kwargs): - pass - - def gui_button_press(self, *args): - pass - - def apply_settings(self): - pass - - -class MessagingModule(BaseModule): - def __init__(self, *args, **kwargs): - BaseModule.__init__(self, *args, **kwargs) - - def process_message(self, message, queue, **kwargs): - return message - - -class ChatModule(BaseModule): - def __init__(self, *args, **kwargs): - BaseModule.__init__(self, *args, **kwargs) diff --git a/modules/helper/parser.py b/modules/helper/parser.py index c1e2187..ffa2a59 100644 --- a/modules/helper/parser.py +++ b/modules/helper/parser.py @@ -1,47 +1,37 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov import os from ConfigParser import RawConfigParser -from collections import OrderedDict - - -def self_heal(conf_file, heal_dict): - heal_config = get_config(conf_file) - for section, section_value in heal_dict.items(): - if not heal_config.has_section(section): - heal_config.add_section(section) - if type(section_value) in [OrderedDict, dict]: - if section_value: - for item, value in section_value.items(): - if not heal_config.has_option(section, item): - heal_config.set(section, item, value) - for item, value in heal_config.items(section): - heal_dict[section][item] = return_type(value) - else: - heal_dict[section] = OrderedDict() - for item, value in heal_config.items(section): - heal_dict[section][item] = value + + +def load_from_config_file(conf_file, conf_dict): + config_parser = get_config(conf_file) + + for section in config_parser.sections(): + if section not in conf_dict.keys(): + continue + + if isinstance(conf_dict[section], dict): + tmp_dict = {} + for item, value in config_parser.items(section): + tmp_dict[item] = return_type(value) + conf_dict[section].update(tmp_dict) + elif isinstance(conf_dict[section], list): + pass else: - if len(heal_config.items(section)) != 1: - for r_item, r_value in heal_config.items(section): - heal_config.remove_option(section, r_item) - heal_config.set(section, section_value) - else: - heal_dict[section] = heal_config.items(section)[0][0] - - heal_config.write(open(conf_file, 'w')) - return heal_config + conf_dict[section] = return_type(config_parser.items(section)[0][0]) + return config_parser def return_type(item): if item: - try: - if isinstance(item, bool): - return item - return int(item) - except: - if item.lower() == 'true': - return True - elif item.lower() == 'false': - return False + if isinstance(item, bool): + return item + elif isinstance(item, int): + return str(item) + elif item.lower() == 'true': + return True + elif item.lower() == 'false': + return False return item @@ -54,3 +44,30 @@ def get_config(conf_file): if os.path.exists(conf_file): heal_config.read(conf_file) return heal_config + + +def save_settings(conf_dict, ignored_sections=()): + if 'parser' not in conf_dict: + return + if 'config' not in conf_dict: + return + + parser = conf_dict.get('parser') # type: RawConfigParser + config = conf_dict.get('config') + + for section, section_object in config.iteritems(): + if section in ignored_sections: + continue + + if parser.has_section(section): + parser.remove_section(section) + parser.add_section(section) + + if isinstance(section_object, dict): + for item, value in section_object.iteritems(): + parser.set(section, item, value) + else: + parser.set(section, section_object) + + with open(conf_dict.get('file'), 'w+') as conf_file: + parser.write(conf_file) diff --git a/modules/helper/system.py b/modules/helper/system.py index 92f27ce..53c3f38 100644 --- a/modules/helper/system.py +++ b/modules/helper/system.py @@ -1,6 +1,9 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov import logging +import random import re import os +import string log = logging.getLogger('system') @@ -10,20 +13,27 @@ SOURCE_USER = 'System' SOURCE_ICON = '/img/sources/lalka_cup.png' +NA_MESSAGE = 'N/A' + +IGNORED_TYPES = ['command', 'system_message'] TRANSLATIONS = {} SPLIT_TRANSLATION = '=' MODULE_KEY = '.' +EMOTE_FORMAT = u':emote;{0}:' TRANSLATION_FILETYPE = '.key' DEFAULT_LANGUAGE = 'en' ACTIVE_LANGUAGE = None +REPLACE_SYMBOLS = '<>' + -def system_message(message, queue, source=SOURCE, icon=SOURCE_ICON, from_user=SOURCE_USER): +def system_message(message, queue, source=SOURCE, icon=SOURCE_ICON, from_user=SOURCE_USER, category='system'): queue.put({'source': source, 'source_icon': icon, 'user': from_user, - 'text': message, - 'system_msg': True}) + 'text': cleanup_tags(message), + 'category': category, + 'type': 'system_message'}) class ModuleLoadException(Exception): @@ -61,8 +71,9 @@ def load_language(language_folder): def find_key_translation(item): translation = TRANSLATIONS.get(item) if translation is None: - if len(item.split(MODULE_KEY)) > 2: - wildcard_item = MODULE_KEY.join([split for split in item.split(MODULE_KEY) if split != '*'][1:]) + split_item = [f_item for f_item in item.split(MODULE_KEY) if f_item != '*'] + if len(split_item) > 1: + wildcard_item = MODULE_KEY.join(split_item[1:]) return find_key_translation('*{0}{1}'.format(MODULE_KEY, wildcard_item)) else: return item @@ -89,3 +100,33 @@ def translate_key(item): def translate(text): pass + + +def cleanup_tags(message): + for symbol in REPLACE_SYMBOLS: + message.replace(symbol, '\{0}'.format(symbol)) + return message + + +def random_string(length): + return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)) + + +def remove_message_by_user(user, text=None): + command = {'type': 'command', + 'command': 'remove_by_user', + 'user': user} + if text: + command['text'] = text + command['command'] = 'replace_by_user' + return command + + +def remove_message_by_id(ids, text=None): + command = {'type': 'command', + 'command': 'remove_by_id', + 'ids': ids} + if text: + command['text'] = text + command['command'] = 'replace_by_id' + return command diff --git a/modules/messaging/blacklist.py b/modules/messaging/blacklist.py index cce810e..8244d60 100644 --- a/modules/messaging/blacklist.py +++ b/modules/messaging/blacklist.py @@ -1,18 +1,17 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import re import os from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.module import MessagingModule +from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 30 class blacklist(MessagingModule): - users = {} - words = {} - def __init__(self, conf_folder, **kwargs): MessagingModule.__init__(self) # Dwarf professions. @@ -43,61 +42,33 @@ def __init__(self, conf_folder, **kwargs): 'view': 'list', 'addable': 'true'}, 'non_dynamic': ['main.*']} - config = self_heal(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id'), - 'config': OrderedDict(conf_dict), - 'gui': conf_gui} - - for item in config.sections(): - for param, value in config.items(item): - if item == 'main': - if param == 'message': - self.message = value.decode('utf-8') - elif item == 'users_hide': - self.users[param] = 'h' - elif item == 'words_hide': - self.words[param] = 'h' - elif item == 'users_block': - self.users[param] = 'b' - elif item == 'words_block': - self.words[param] = 'b' + config = load_from_config_file(conf_file, conf_dict) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': conf_dict['gui_information']['id'], + 'config': OrderedDict(conf_dict), + 'gui': conf_gui}) def process_message(self, message, queue, **kwargs): if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message - user = self.blacklist_user_handler(message) - # True = Hide, False = Del, None = Do Nothing - if user: - message['text'] = self.message - elif user is False: - return - words = self.blacklist_message_handler(message) - if words: - message['text'] = self.message - elif words is False: + if message['user'].lower() in self._conf_params['config']['users_hide'].keys(): return - return message + for word in self._conf_params['config']['words_hide']: + if re.search(word, message['text'].encode('utf-8')): + return - def blacklist_user_handler(self, message): - user = message.get('user').lower() - if user in self.users: - if self.users[user] == 'h': - return True - else: - return False - return None + if message['user'].lower() in self._conf_params['config']['users_block'].keys(): + message['text'] = self._conf_params['config']['main']['message'] + return message - def blacklist_message_handler(self, message): - for word in self.words: - if re.search(word, message['text'].encode('utf-8')): - if self.words[word] == 'h': - return True - else: - return False - return None + for word in self._conf_params['config']['words_block']: + if re.search(word, message['text'].encode('utf-8')): + message['text'] = self._conf_params['config']['main']['message'] + return message + return message diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 451322f..3d5601f 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -1,12 +1,14 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import logging import os import random import re from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.module import MessagingModule +from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 10 log = logging.getLogger('c2b') @@ -49,34 +51,29 @@ def __init__(self, conf_folder, **kwargs): 'addable': 'true', 'view': 'list_dual'}, 'non_dynamic': ['config.*']} - config = self_heal(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id'), - 'config': conf_dict, - 'gui': conf_gui} - - tag_config = 'config' - self.f_items = [] - for param, value in config.items(tag_config): - f_item = {'filter': param.decode('utf-8'), 'replace': value.split('/')} - f_item['replace'] = [item.strip().decode('utf-8') for item in f_item['replace']] - self.f_items.append(f_item) + config = load_from_config_file(conf_file, conf_dict) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': conf_dict['gui_information']['id'], + 'config': conf_dict, + 'gui': conf_gui}) def process_message(self, message, queue, **kwargs): # Replacing the message if needed. # Please do the needful if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message - for replace in self.f_items: - if replace['filter'] in message['text']: - replace_word = random.choice(replace['replace']) - # Fix twitch emoticons if any + + for item, replace in self._conf_params['config']['config'].iteritems(): + item = item.decode('utf-8') + if item in message['text']: + replace_word = random.choice(replace.split('/')).decode('utf-8') if message['source'] == 'tw': - message['emotes'] = twitch_replace_indexes(replace['filter'], message['text'], - len(replace['filter']), len(replace_word), + message['emotes'] = twitch_replace_indexes(item, message['text'], + len(item), len(replace_word), message.get('emotes', [])) - message['text'] = message['text'].replace(replace['filter'], replace_word) + message['text'] = message['text'].replace(item, replace_word) return message diff --git a/modules/messaging/df.py b/modules/messaging/df.py index 8f05ad8..5e6f0b7 100644 --- a/modules/messaging/df.py +++ b/modules/messaging/df.py @@ -1,11 +1,13 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import re import os from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.module import MessagingModule +from modules.helper.system import IGNORED_TYPES class df(MessagingModule): @@ -19,24 +21,22 @@ def __init__(self, conf_folder, **kwargs): conf_dict['grep'] = OrderedDict() conf_dict['grep']['symbol'] = '#' conf_dict['grep']['file'] = 'logs/df.txt' - conf_dict['prof'] = {'nothing': '([Нн]икто|[Nn]othing|\w*)'} + conf_dict['prof'] = OrderedDict() conf_gui = { 'prof': { 'view': 'list_dual', 'addable': True}, 'non_dynamic': ['grep.*']} - config = self_heal(conf_file, conf_dict) - grep_tag = 'grep' - prof_tag = 'prof' + config = load_from_config_file(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui} - self.symbol = config.get(grep_tag, 'symbol') - self.file = config.get(grep_tag, 'file') + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui}) + self.file = conf_dict['grep']['file'] dir_name = os.path.dirname(self.file) if not os.path.exists(dir_name): @@ -46,26 +46,21 @@ def __init__(self, conf_folder, **kwargs): with open(self.file, 'w'): pass - self.prof = [] - for prof, regex in config.items(prof_tag): - comp = [prof.capitalize(), self.symbol + regex.decode('utf-8')] - self.prof.append(comp) - - def write_to_file(self, message): + def write_to_file(self, user, role): with open(self.file, 'r') as f: for line in f.readlines(): - if message['user'] == line.split(',')[0]: + if user == line.split(',')[0]: return - with open(self.file, 'a') as a_file: - a_file.write("{0},{1}\n".format(message['user'], message['text'])) + with open(self.file, 'a') as a_file: + a_file.write("{0},{1}\n".format(user, role)) def process_message(self, message, queue, **kwargs): if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message - for regexp in self.prof: - if re.search(regexp[1], message['text']): - comp = {'user': message['user'], 'text': regexp[0]} - self.write_to_file(comp) + for role, regexp in self._conf_params['config']['prof'].iteritems(): + if re.search('{0}{1}'.format(self._conf_params['config']['grep']['symbol'], regexp).decode('utf-8'), + message['text']): + self.write_to_file(message['user'], role.capitalize()) break return message diff --git a/modules/messaging/levels.py b/modules/messaging/levels.py index dd3972c..444e409 100644 --- a/modules/messaging/levels.py +++ b/modules/messaging/levels.py @@ -1,5 +1,6 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import logging import math import os @@ -9,9 +10,9 @@ from collections import OrderedDict import datetime -from modules.helper.parser import self_heal -from modules.helper.system import system_message, ModuleLoadException -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.system import system_message, ModuleLoadException, IGNORED_TYPES, random_string +from modules.helper.module import MessagingModule log = logging.getLogger('levels') @@ -41,36 +42,39 @@ def __init__(self, conf_folder, **kwargs): conf_dict['config']['exp_for_level'] = 200 conf_dict['config']['exp_for_message'] = 1 conf_dict['config']['decrease_window'] = 60 - conf_gui = {'non_dynamic': ['config.*'], + conf_gui = {'non_dynamic': ['config.db', 'config.experience', + 'config.exp_for_level', 'config.exp_for_message', + 'decrease_window'], 'config': { 'experience': { 'view': 'dropdown', 'choices': ['static', 'geometrical', 'random']}}} - config = self_heal(conf_file, conf_dict) + config = load_from_config_file(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui} + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui}) self.conf_folder = None self.experience = None self.exp_for_level = None self.exp_for_message = None - self.filename = None + self.level_file = None self.levels = None self.special_levels = None self.db_location = None - self.message = None self.decrease_window = None self.threshold_users = None def load_module(self, *args, **kwargs): - main_settings = kwargs.get('main_settings') - loaded_modules = kwargs.get('loaded_modules') - if 'webchat' not in loaded_modules: + MessagingModule.load_module(self, *args, **kwargs) + if 'webchat' not in self._loaded_modules: raise ModuleLoadException("Unable to find webchat module that is needed for level module") + else: + self._loaded_modules['webchat']['class'].add_depend('levels') conf_folder = self._conf_params['folder'] conf_dict = self._conf_params['config'] @@ -79,27 +83,42 @@ def load_module(self, *args, **kwargs): self.experience = conf_dict['config'].get('experience') self.exp_for_level = float(conf_dict['config'].get('exp_for_level')) self.exp_for_message = float(conf_dict['config'].get('exp_for_message')) - self.filename = os.path.abspath(os.path.join(loaded_modules['webchat']['style_location'], 'levels.xml')) + self.level_file = os.path.abspath( + os.path.join( + self._loaded_modules['webchat']['style_settings']['location'], 'levels.xml' + ) + ) self.levels = [] self.special_levels = {} self.db_location = os.path.join(conf_dict['config'].get('db')) - self.message = conf_dict['config'].get('message').decode('utf-8') self.decrease_window = int(conf_dict['config'].get('decrease_window')) self.threshold_users = {} # Load levels - if not os.path.exists(self.filename): - log.error("{0} not found, generating from template".format(self.filename)) - raise ModuleLoadException("{0} not found, generating from template".format(self.filename)) + if not os.path.exists(self.level_file): + log.error("{0} not found, generating from template".format(self.level_file)) + raise ModuleLoadException("{0} not found, generating from template".format(self.level_file)) if self.experience == 'random': self.db_location += '.random' self.create_db(self.db_location) - tree = ElementTree.parse(os.path.join(conf_folder, self.filename)) - lvl_xml = tree.getroot() + self.load_levels() + + def load_levels(self): + if self.levels: + self.levels = [] - for level_data in lvl_xml: + if self.special_levels: + self.special_levels = {} + + self.level_file = os.path.abspath( + os.path.join( + self._loaded_modules['webchat']['style_settings']['location'], 'levels.xml' + ) + ) + tree = ElementTree.parse(self.level_file) + for level_data in tree.getroot(): level_count = float(len(self.levels) + 1) if 'nick' in level_data.attrib: self.special_levels[level_data.attrib['nick']] = level_data.attrib @@ -109,8 +128,13 @@ def load_module(self, *args, **kwargs): else: level_exp = self.exp_for_level * level_count level_data.attrib['exp'] = level_exp + level_data.attrib['url'] = '{0}?{1}'.format(level_data.attrib['url'], random_string(5)) self.levels.append(level_data.attrib) + def apply_settings(self, **kwargs): + if 'webchat' in kwargs.get('from_depend', []): + self.load_levels() + def set_level(self, user, queue): if user == 'System': return [] @@ -147,13 +171,18 @@ def set_level(self, user, queue): db.commit() else: max_level += 1 - system_message(self.message.format(user, self.levels[max_level]['name']), queue) + system_message( + self._conf_params['config']['config']['message'].decode('utf-8').format( + user, + self.levels[max_level]['name']), + queue, category='module' + ) cursor.close() return self.levels[max_level] def process_message(self, message, queue, **kwargs): if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message if 'system_msg' not in message or not message['system_msg']: if 'user' in message and message['user'] in self.special_levels: diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index e438b22..5260f0b 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -1,11 +1,13 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import os import datetime from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.module import MessagingModule +from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 20 @@ -27,20 +29,19 @@ def __init__(self, conf_folder, **kwargs): conf_dict['config']['rotation'] = 'daily' conf_gui = {'non_dynamic': ['config.*']} - config = self_heal(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id'), - 'config': conf_dict, - 'gui': conf_gui} + config = load_from_config_file(conf_file, conf_dict) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': conf_dict['gui_information']['id'], + 'config': conf_dict, + 'gui': conf_gui}) - tag_config = 'config' - - self.format = config.get(tag_config, 'file_format') - self.ts_format = config.get(tag_config, 'message_date_format') - self.logging = config.get(tag_config, 'logging') - self.rotation = config.get(tag_config, 'logging') + self.format = conf_dict['config']['file_format'] + self.ts_format = conf_dict['config']['message_date_format'] + self.logging = conf_dict['config']['logging'] + self.rotation = conf_dict['config']['rotation'] self.folder = 'logs' @@ -50,12 +51,13 @@ def __init__(self, conf_folder, **kwargs): def process_message(self, message, queue, **kwargs): if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message with open('{0}.txt'.format( os.path.join(self.destination, datetime.datetime.now().strftime(self.format))), 'a') as f: - f.write('[{3}] [{0}] {1}: {2}\n'.format(message['source'].encode('utf-8'), - message['user'].encode('utf-8'), - message['text'].encode('utf-8'), - datetime.datetime.now().strftime(self.ts_format).encode('utf-8'))) + f.write('[{3}] [{0}] {1}: {2}\n'.format( + message['source'].encode('utf-8'), + message['user'].encode('utf-8'), + message['text'].encode('utf-8'), + datetime.datetime.now().strftime(self.ts_format).encode('utf-8'))) return message diff --git a/modules/messaging/mentions.py b/modules/messaging/mentions.py index 5b46b9a..cba8597 100644 --- a/modules/messaging/mentions.py +++ b/modules/messaging/mentions.py @@ -1,11 +1,13 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- +# Copyright (C) 2016 CzT/Vladislav Ivanov import os import re from collections import OrderedDict -from modules.helper.parser import self_heal -from modules.helper.modules import MessagingModule +from modules.helper.parser import load_from_config_file +from modules.helper.module import MessagingModule +from modules.helper.system import IGNORED_TYPES class mentions(MessagingModule): @@ -24,39 +26,31 @@ def __init__(self, conf_folder, **kwargs): 'view': 'list'}, 'address': { 'addable': 'true', - 'view': 'list'}, - 'non_dynamic': ['mentions.*', 'address.*']} - config = self_heal(conf_file, conf_dict) - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui} - mention_tag = 'mentions' - address_tag = 'address' - if config.has_section(mention_tag): - self.mentions = [item for item, value in config.items(mention_tag)] - else: - self.mentions = [] - - if config.has_section(address_tag): - self.addresses = [item.decode('utf-8').lower() for item, value in config.items(address_tag)] - else: - self.addresses = [] + 'view': 'list'}} + config = load_from_config_file(conf_file, conf_dict) + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui}) def process_message(self, message, queue, **kwargs): # Replacing the message if needed. # Please do the needful if message: - if 'command' in message: + if message['type'] in IGNORED_TYPES: return message - for mention in self.mentions: + + for mention, value in self._conf_params['config']['mentions'].iteritems(): if re.search(mention, message['text'].lower()): message['mention'] = True + break - for address in self.addresses: + for address, value in self._conf_params['config']['address'].iteritems(): if re.match(address, message['text'].lower()): message['pm'] = True + break if 'mention' in message and 'pm' in message: message.pop('mention') diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 66f91b8..d6358f3 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -1,3 +1,4 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov import os import threading import json @@ -5,20 +6,22 @@ import socket import cherrypy import logging +import datetime from collections import OrderedDict from jinja2 import Template from cherrypy.lib.static import serve_file -from time import sleep from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket -from modules.helper.parser import self_heal +from modules.helper.parser import load_from_config_file, save_settings from modules.helper.system import THREADS -from modules.helper.modules import MessagingModule +from modules.helper.module import MessagingModule from gui import MODULE_KEY from main import PYTHON_FOLDER, CONF_FOLDER +DEFAULT_STYLE = 'default' DEFAULT_PRIORITY = 9001 HISTORY_SIZE = 20 +HISTORY_TYPES = ['system_message', 'message'] HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") s_queue = Queue.Queue() logging.getLogger('ws4py').setLevel(logging.ERROR) @@ -26,66 +29,103 @@ class MessagingThread(threading.Thread): - def __init__(self): + def __init__(self, settings): super(self.__class__, self).__init__() self.daemon = True + self.settings = settings + self.running = True def run(self): - while True: + while self.running: message = s_queue.get() - cherrypy.engine.publish('websocket-broadcast', json.dumps(message)) - if 'command' not in message: + if 'timestamp' not in message: + message['timestamp'] = datetime.datetime.now().isoformat() + + if message['type'] in HISTORY_TYPES: cherrypy.engine.publish('add-history', message) + elif message['type'] == 'command': + cherrypy.engine.publish('process-command', message['command'], message) + + if message['type'] == 'system_message' and not self.settings['show_system_msg']: + continue + + cherrypy.engine.publish('websocket-broadcast', json.dumps(message)) + log.info("Messaging thread stopping") + + def stop(self): + self.running = False class FireFirstMessages(threading.Thread): - def __init__(self, ws, history): + def __init__(self, ws, history, settings): super(self.__class__, self).__init__() self.daemon = True - self.ws = ws + self.ws = ws # type: WebChatSocketServer self.history = history + self.settings = settings def run(self): - sleep(0.1) - for item in self.history: - if item: + show_system_msg = cherrypy.engine.publish('get-settings')[0]['show_system_msg'] + if self.ws.stream: + for item in self.history: + if item['type'] == 'system_message' and not show_system_msg: + continue + timestamp = datetime.datetime.strptime(item['timestamp'], "%Y-%m-%dT%H:%M:%S.%f") + timedelta = datetime.datetime.now() - timestamp + timer = int(self.settings['timer']) + if timer > 0: + if timedelta > datetime.timedelta(seconds=timer): + continue self.ws.send(json.dumps(item)) class WebChatSocketServer(WebSocket): def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbeat_freq=None): WebSocket.__init__(self, sock) + self.daemon = True self.clients = [] + self.settings = cherrypy.engine.publish('get-settings') def opened(self): cherrypy.engine.publish('add-client', self.peer_address, self) - send_history = FireFirstMessages(self, cherrypy.engine.publish('get-history')[0]) - send_history.start() + timer = threading.Timer(0.3, self.fire_history) + timer.start() def closed(self, code, reason=None): cherrypy.engine.publish('del-client', self.peer_address) + def fire_history(self): + send_history = FireFirstMessages(self, cherrypy.engine.publish('get-history')[0], + cherrypy.engine.publish('get-settings')[0]) + send_history.start() + class WebChatPlugin(WebSocketPlugin): - def __init__(self, bus): + def __init__(self, bus, settings): WebSocketPlugin.__init__(self, bus) + self.daemon = True self.clients = [] + self.style_settings = settings self.history = [] self.history_size = HISTORY_SIZE def start(self): WebSocketPlugin.start(self) + self.bus.subscribe('get-settings', self.get_settings) self.bus.subscribe('add-client', self.add_client) self.bus.subscribe('del-client', self.del_client) self.bus.subscribe('add-history', self.add_history) self.bus.subscribe('get-history', self.get_history) + self.bus.subscribe('process-command', self.process_command) def stop(self): WebSocketPlugin.stop(self) + self.bus.unsubscribe('get-settings', self.get_settings) self.bus.unsubscribe('add-client', self.add_client) self.bus.unsubscribe('del-client', self.del_client) self.bus.unsubscribe('add-history', self.add_history) self.bus.unsubscribe('get-history', self.get_history) + self.bus.ubsubscribe('process-command', self.process_command) def add_client(self, addr, websocket): self.clients.append({'ip': addr[0], 'port': addr[1], 'websocket': websocket}) @@ -97,38 +137,108 @@ def del_client(self, addr): pass def add_history(self, message): - message['history'] = True self.history.append(message) if len(self.history) > self.history_size: self.history.pop(0) + def get_settings(self): + return self.style_settings['keys'] + def get_history(self): return self.history + def process_command(self, command, values): + if command == 'remove_by_id': + self._remove_by_id(values['ids']) + elif command == 'remove_by_user': + self._remove_by_user(values['user']) + elif command == 'replace_by_id': + self._replace_by_id(values['ids']) + elif command == 'replace_by_user': + self._replace_by_user(values['user']) + + def _remove_by_id(self, ids): + for item in ids: + for message in self.history: + if message.get('id') == item: + self.history.remove(message) + + def _remove_by_user(self, users): + for item in users: + for message in reversed(self.history): + if message.get('item') == item: + self.history.remove(message) + + def _replace_by_id(self, ids): + for item in ids: + for index, message in enumerate(self.history): + if message.get('id') == item: + self.history[index]['text'] = self.style_settings['keys']['remove_text'] + if 'emotes' in self.history[index]: + del self.history[index]['emotes'] + if 'bttv_emotes' in self.history[index]: + del self.history[index]['bttv_emotes'] + + def _replace_by_user(self, users): + for item in users: + for index, message in enumerate(self.history): + if message.get('user') == item: + self.history[index]['text'] = self.style_settings['keys']['remove_text'] + if 'emotes' in self.history[index]: + del self.history[index]['emotes'] + if 'bttv_emotes' in self.history[index]: + del self.history[index]['bttv_emotes'] + + +class RestRoot(object): + def __init__(self, settings, modules): + self.settings = settings + self._rest_modules = {} + + for name, params in modules.iteritems(): + module_class = params.get('class', None) + if module_class: + api = params['class'].rest_api() + if api: + self._rest_modules[name] = api + + @cherrypy.expose + def default(self, *args): + if len(args) > 0: + module_name = args[0] + query = args[1:] + if module_name in self._rest_modules: + method = cherrypy.request.method + api = self._rest_modules[module_name] + if method in api: + return api[method](query) + return json.dumps({'error': 'Bad Request', + 'status': 400, + 'message': 'Unable to find module'}) + class CssRoot(object): - def __init__(self, http_folder, settings): - object.__init__(self) - self.http_folder = http_folder + def __init__(self, settings): self.settings = settings - @cherrypy.expose() + @cherrypy.expose def style_css(self): - with open(os.path.join(self.http_folder, 'css', 'style.css'), 'r') as css: - return Template(css.read()).render(**self.settings) + cherrypy.response.headers['Content-Type'] = 'text/css' + with open(os.path.join(self.settings['location'], 'css', 'style.css'), 'r') as css: + css_content = css.read() + return Template(css_content).render(**self.settings['keys']) class HttpRoot(object): - def __init__(self, http_folder): - object.__init__(self) - self.http_folder = http_folder + def __init__(self, style_settings): + self.settings = style_settings @cherrypy.expose def index(self): cherrypy.response.headers["Expires"] = -1 cherrypy.response.headers["Pragma"] = "no-cache" cherrypy.response.headers["Cache-Control"] = "private, max-age=0, no-cache, no-store, must-revalidate" - return serve_file(os.path.join(self.http_folder, 'index.html'), 'text/html') + return serve_file(os.path.join(self.settings['location'], 'index.html'), 'text/html') @cherrypy.expose def ws(self): @@ -143,16 +253,38 @@ def __init__(self, host, port, root_folder, **kwargs): self.host = host self.port = port self.root_folder = root_folder - self.style = kwargs.pop('style') - self.settings = kwargs.pop('settings') + self.style_settings = kwargs['style_settings'] + self.modules = kwargs.pop('modules') + + self.root_config = None + self.css_config = None + self.rest_config = None cherrypy.config.update({'server.socket_port': int(self.port), 'server.socket_host': self.host, 'engine.autoreload.on': False}) - WebChatPlugin(cherrypy.engine).subscribe() + self.websocket = WebChatPlugin(cherrypy.engine, self.style_settings) + self.websocket.subscribe() cherrypy.tools.websocket = WebSocketTool() + def update_settings(self): + self.root_config = { + '/ws': {'tools.websocket.on': True, + 'tools.websocket.handler_cls': WebChatSocketServer}, + '/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(self.style_settings['location'], 'js')}, + '/img': {'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(self.style_settings['location'], 'img'), + 'tools.caching.on': True, + 'tools.expires.on': True, + 'tools.expires.secs': 1}} + self.css_config = { + '/': {} + } + self.rest_config = { + '/': {} + } + def run(self): - http_folder = self.style cherrypy.log.access_file = '' cherrypy.log.error_file = '' cherrypy.log.screen = False @@ -161,31 +293,14 @@ def run(self): cherrypy.log.access_log.propagate = False cherrypy.log.error_log.setLevel(logging.ERROR) - config = { - '/ws': {'tools.websocket.on': True, - 'tools.websocket.handler_cls': WebChatSocketServer}, - '/js': {'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(http_folder, 'js')}, - '/img': {'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(http_folder, 'img')}} - - css_config = { - '/': {} - } - - cherrypy.tree.mount(HttpRoot(http_folder), '', config) - cherrypy.tree.mount(CssRoot(http_folder, self.settings), '/css', css_config) - + self.update_settings() + self.mount_dirs() cherrypy.engine.start() - cherrypy.engine.block() - # cherrypy.quickstart(HttpRoot(http_folder), '/', - # config={'/ws': {'tools.websocket.on': True, - # 'tools.websocket.handler_cls': WebChatSocketServer}, - # '/js': {'tools.staticdir.on': True, - # 'tools.staticdir.dir': os.path.join(http_folder, 'js')}, - # '/img': {'tools.staticdir.on': True, - # 'tools.staticdir.dir': os.path.join(http_folder, 'img')}}) + def mount_dirs(self): + cherrypy.tree.mount(HttpRoot(self.style_settings), '', self.root_config) + cherrypy.tree.mount(CssRoot(self.style_settings), '/css', self.css_config) + cherrypy.tree.mount(RestRoot(self.style_settings, self.modules), '/rest', self.rest_config) def socket_open(host, port): @@ -197,6 +312,7 @@ def socket_open(host, port): class webchat(MessagingModule): def __init__(self, conf_folder, **kwargs): MessagingModule.__init__(self) + # Module configuration conf_file = os.path.join(conf_folder, "webchat.cfg") conf_dict = OrderedDict() conf_dict['gui_information'] = { @@ -207,64 +323,102 @@ def __init__(self, conf_folder, **kwargs): conf_dict['server']['host'] = '127.0.0.1' conf_dict['server']['port'] = '8080' conf_dict['style'] = 'czt' - conf_dict['style_settings'] = { - 'font_size': 15 - } + conf_dict['style_settings'] = OrderedDict() + conf_gui = { 'style': { 'check': 'http', 'check_type': 'dir', 'view': 'choose_single'}, - 'style_settings': { - 'font_size': {'view': 'spin', - 'min': 10, - 'max': 100}}, - 'non_dynamic': ['server.*']} - - config = self_heal(conf_file, conf_dict) + 'style_settings': {}, + 'non_dynamic': ['server.*'], + 'ignored_sections': ['style_settings'] + } - fallback_style = 'czt' - path = os.path.abspath(os.path.join(HTTP_FOLDER, conf_dict['style'])) - if os.path.exists(path): - style_location = path - else: - style_location = os.path.join(HTTP_FOLDER, fallback_style) - - self._conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id'), - 'config': conf_dict, - 'gui': conf_gui, - 'host': conf_dict['server']['host'], - 'port': conf_dict['server']['port'], - 'style_location': style_location} + parser = load_from_config_file(conf_file, conf_dict) + + style_path = self.get_style_path(conf_dict['style']) + + self._conf_params.update( + {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': parser, + 'id': conf_dict['gui_information']['id'], + 'config': conf_dict, + 'gui': conf_gui, + 'host': conf_dict['server']['host'], + 'port': conf_dict['server']['port'], + 'style_settings': { + 'name': conf_dict['style'], + 'location': style_path, + 'settings_location': os.path.join(style_path, 'settings.json'), + 'settings_gui_location': os.path.join(style_path, 'settings_gui.json'), + 'keys': OrderedDict([ + ('show_system_msg', False) + ]) + }}) + + self.load_style_settings() + + self.s_thread = None self.queue = None self.message_threads = [] + # Rest Api Settings + self._rest_api['GET'] = self.rest_get + def load_module(self, *args, **kwargs): + MessagingModule.load_module(self, *args, **kwargs) self.queue = kwargs.get('queue') - conf_dict = self._conf_params - host = conf_dict['host'] - port = conf_dict['port'] + self.start_webserver() + def start_webserver(self): + host = self._conf_params['host'] + port = self._conf_params['port'] if socket_open(host, port): - s_thread = SocketThread(host, port, CONF_FOLDER, style=self._conf_params['style_location'], - settings=self._conf_params['config']['style_settings']) - s_thread.start() + self.s_thread = SocketThread(host, port, CONF_FOLDER, + style_settings=self._conf_params['style_settings'], + modules=self._loaded_modules) + self.s_thread.start() for thread in range(THREADS+5): - self.message_threads.append(MessagingThread()) + self.message_threads.append(MessagingThread(self._conf_params['style_settings']['keys'])) self.message_threads[thread].start() else: log.error("Port is already used, please change webchat port") + @staticmethod + def get_style_path(style): + path = os.path.abspath(os.path.join(HTTP_FOLDER, style)) + if os.path.exists(path): + style_location = path + else: + style_location = os.path.join(HTTP_FOLDER, DEFAULT_STYLE) + return style_location + def reload_chat(self): - self.queue.put({'command': 'reload'}) + self.queue.put({'type': 'command', 'command': 'reload'}) + + def apply_settings(self, **kwargs): + save_settings(self.conf_params(), ignored_sections=self._conf_params['gui'].get('ignored_sections')) + if 'system_exit' in kwargs: + return - def apply_settings(self): + self.update_style_settings() self.reload_chat() + if self._conf_params['config']['style'] != self._conf_params['style_settings']['name']: + log.info("changing style") + style_name = self._conf_params['config']['style'] + self._conf_params['style_settings']['name'] = style_name + self._conf_params['style_settings']['location'] = self.get_style_path(style_name) + self.s_thread.update_settings() + self.s_thread.mount_dirs() + + if self._conf_params['dependencies']: + for module in self._conf_params['dependencies']: + self._loaded_modules[module]['class'].apply_settings(from_depend='webchat') + def gui_button_press(self, gui_module, event, list_keys): log.debug("Received button press for id {0}".format(event.GetId())) keys = MODULE_KEY.join(list_keys) @@ -275,7 +429,31 @@ def gui_button_press(self, gui_module, event, list_keys): def process_message(self, message, queue, **kwargs): if message: if 'flags' in message: - if message['flags'] == 'hidden': + if 'hidden' in message['flags']: return message s_queue.put(message) return message + + def rest_get(self, *args): + return json.dumps(self._conf_params['style_settings']['keys']) + + def load_style_settings(self): + file_path = self._conf_params['style_settings']['settings_location'] + if os.path.exists(file_path): + with open(file_path, 'r') as style_file: + self._conf_params['style_settings']['keys'].update(json.loads(style_file.read(), + object_pairs_hook=OrderedDict)) + + self._conf_params['style_settings']['keys'].update(self._conf_params['config']['style_settings']) + self._conf_params['config']['style_settings'].update(self._conf_params['style_settings']['keys']) + + gui_file_path = self._conf_params['style_settings']['settings_gui_location'] + if os.path.exists(gui_file_path): + with open(gui_file_path, 'r') as gui_file: + self._conf_params['gui']['style_settings'].update(json.loads(gui_file.read())) + + def update_style_settings(self): + self._conf_params['style_settings']['keys'].update(self._conf_params['config']['style_settings']) + file_path = self._conf_params['style_settings']['settings_location'] + with open(file_path, 'w') as style_file: + style_file.write(json.dumps(self._conf_params['style_settings']['keys'], indent=2)) diff --git a/setup.py b/setup.py index 720ea60..566e205 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='LalkaChat', version=VERSION, - packages=['', 'modules', 'modules.helpers', 'modules.messaging'], + packages=['', 'modules', 'modules.helper', 'modules.messaging'], requires=['requests', 'cherrypy', 'ws4py', 'irc', 'wxpython', 'cefpython3', 'semantic_version', 'jinja2'], url='https://github.com/DeForce/LalkaChat', license='', diff --git a/src/themes/default/app/app.js b/src/themes/default/app/app.js new file mode 100644 index 0000000..bfef8a0 --- /dev/null +++ b/src/themes/default/app/app.js @@ -0,0 +1,209 @@ +const Vue = require('vue'); +const DOMPurify = require('dompurify'); + +(function (WebSocket, Vue, Sanitizer) { + 'use strict'; + + new Vue({ + el: '#chat-container', + data: function () { + var wsUrl = 'ws://' + window.location.host + '/ws'; + var messages = []; + var socket = new WebSocket(wsUrl); + + return { + messages: messages, + url: wsUrl, + socket: socket, + attempts: 0, + socketInterval: null, + messagesInterval: -1, + messagesLimit: 30 + } + }, + created: function () { + var self = this; + + self.socket.onmessage = this.onmessage; + self.socket.onopen = this.onopen; + self.socket.onclose = this.onclose; + + self.get(window.location.href + 'rest/webchat', function (err, response) { + if (!err) { + self.messagesInterval = response.timer * 1000 || -1; + } + + if (self.messagesInterval > 0) { + setInterval(self.clear, 500); + } + }); + }, + methods: { + mouseenter: function (message) { + message.deleteButton = true; + }, + mouseleave: function (message) { + message.deleteButton = false; + }, + clear: function () { + var that = this; + var time = new Date(); + + this.messages = this.messages.filter(function (message) { + return Math.abs(time - message.time) < that.messagesInterval; + }); + }, + remove: function (message) { + var index = this.messages.indexOf(message); + if (index >= 0) { + this.messages.splice(index, 1); + } + }, + sanitize: function (message) { + var html = this.replaceDefaultEmotions(message.text, message.emotes); + var clean = Sanitizer.sanitize(html); + + if (!clean) this.remove(message); + + return clean; + }, + replaceDefaultEmotions: function (message, emotes) { + if (!emotes || emotes.length <= 0) { + return message; + } + return message.replace(/:emote;(\w+|\d+):/g, function (code, emote_key) { + for (var emote in emotes) { + if (!!emotes[emote] && emotes[emote]['emote_id'] == emote_key) { + return ''; + } + } + + return code; + }); + }, + removeByIds: function (ids) { + this.messages = this.messages.filter(function (message) { + return ids.indexOf(message.id) < 0; + }); + }, + removeByUsernames: function (usernames) { + usernames = usernames.map(function (value) { + return value.toLowerCase(); + }); + + this.messages = this.messages.filter(function (message) { + var user = message.user.toLowerCase(); + return usernames.indexOf(user) < 0; + }); + }, + replaceByUsernames: function (command) { + var usernames = command.user.map(function(value) { + return value.toLowerCase(); + }); + + this.messages = this.messages.map(function (message) { + var user = message.user.toLowerCase(); + var index = usernames.indexOf(user); + + if (index >= 0) { + message.text = command.text; + message.emotes = []; + message.bttv_emotes = {}; + } + + return message; + }); + }, + replaceByIds: function (command) { + this.messages = this.messages.map(function (message) { + var index = command.ids.indexOf(message.id); + + if (index >= 0) { + message.text = command.text; + delete message.emotes; + delete message.bttv_emotes; + } + return message; + }); + }, + run: function (message) { + if (!message.command) + return; + + switch (message.command) { + case 'reload': + window.location.reload(); + break; + case 'remove_by_user': + this.removeByUsernames(message.user); + break; + case 'remove_by_id': + this.removeByIds(message.ids); + break; + case 'replace_by_id': + this.replaceByIds(message); + break; + case 'replace_by_user': + this.replaceByUsernames(message); + break; + default: + console.log('Got unknown command ', message.command); + } + }, + onmessage: function (event) { + var message = JSON.parse(event.data); + if (!message.type) + return; + + switch (message.type) { + case 'command': + this.run(message); + break; + default: + message.time = new Date(); + message.deleteButton = false; + this.messages.push(message); + if (this.messages.length > this.messagesLimit) { + this.remove(this.messages[0]); + } + } + }, + onopen: function () { + this.attempts = 0; + if (!this.socketInterval) { + clearInterval(this.socketInterval); + this.socketInterval = null; + } + }, + onclose: function () { + this.socketInterval = setInterval(this.reconnect, 1000); + }, + reconnect: function () { + this.attempts++; + + this.socket = new WebSocket(this.url); + }, + load: function (method, url, callback, data) { + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + var obj = JSON.parse(xhr.responseText); + callback(null, obj); + }; + xhr.onerror = function () { + var obj = JSON.parse(xhr.responseText); + callback(obj); + }; + + xhr.open(method, url); + xhr.send(data); + }, + get: function (url, callback, data) { + return this.load('get', url, callback, data); + }, + post: function (url, data, callback) { + return this.load('post', url, callback, data); + } + }, + filters: {} + }); +})(window.WebSocket, Vue, DOMPurify); diff --git a/src/themes/default/assets/css/style.css b/src/themes/default/assets/css/style.css new file mode 100644 index 0000000..09fbcba --- /dev/null +++ b/src/themes/default/assets/css/style.css @@ -0,0 +1,105 @@ +body { + height: 100%; + overflow: hidden; + padding: 0; + margin: 0; +} + +#chat-container { + position: absolute; + bottom: 0; + width: 100%; +} + +.message { + background-color: rgba( 35, 35, 37, {{ message_opacity|float / 100.00 }}); + word-wrap: break-word; + text-shadow: 0 1px 1px black; + font-family: 'Consolas', serif; + font-size: 0; + padding-top: 2px; + color: #FFF; +} + +.message > div { + display: inline; + font-size: {{ font_size }}pt; +} + +.message.sub-message { + background-color: rgba( 255, 0, 128, 0.62 ) +} + +.message-remove, +.message-source, +.message-level, +.message-badges { + position: relative; + margin: 0 3px 0 0; + top: 3px; + left: 3px; +} + +.delete, +.badge, +.platform, +.level, +.s-level { + background-color: rgba(47, 47, 47, 1); + border-color: rgba(47, 47, 47, 1); + border-width: 2px; + border-style: solid; + /*margin: -3px;*/ + width: {{ badge_size }}px; + height: {{ badge_size }}px; + /*padding: 2px;*/ + border-radius: 5px; +} + +.smile { + margin-top: -4px; + height: {{ smile_size }}px; + width: auto; + top: 2px; +} + + +.text, +.username { + padding-left: 5px; +} + +.private { + color: #e57017; +} + +.system { + color: #ff68fb; +} + +.mention { + color: #c0ffc0; +} + +.broadcaster { + background-color: #e71818; +} + +.mod { + background-color: #34ae0a; +} + +.turbo { + background-color: #6441a5; +} + +.premium { + background-color: #009CDC; +} + +.show-del-enter-active, .show-del-leave-active { + transition: opacity .5s +} +.show-del-enter, .show-del-leave-active { + opacity: 0 +} \ No newline at end of file diff --git a/src/themes/default/assets/img/gui/delete.png b/src/themes/default/assets/img/gui/delete.png new file mode 100644 index 0000000..15edd96 Binary files /dev/null and b/src/themes/default/assets/img/gui/delete.png differ diff --git a/http/czt/img/levels/0.png b/src/themes/default/assets/img/levels/0.png similarity index 100% rename from http/czt/img/levels/0.png rename to src/themes/default/assets/img/levels/0.png diff --git a/http/czt/img/levels/1.png b/src/themes/default/assets/img/levels/1.png similarity index 100% rename from http/czt/img/levels/1.png rename to src/themes/default/assets/img/levels/1.png diff --git a/http/czt/img/levels/10.png b/src/themes/default/assets/img/levels/10.png similarity index 100% rename from http/czt/img/levels/10.png rename to src/themes/default/assets/img/levels/10.png diff --git a/http/czt/img/levels/11.png b/src/themes/default/assets/img/levels/11.png similarity index 100% rename from http/czt/img/levels/11.png rename to src/themes/default/assets/img/levels/11.png diff --git a/http/czt/img/levels/12.png b/src/themes/default/assets/img/levels/12.png similarity index 100% rename from http/czt/img/levels/12.png rename to src/themes/default/assets/img/levels/12.png diff --git a/http/czt/img/levels/13.png b/src/themes/default/assets/img/levels/13.png similarity index 100% rename from http/czt/img/levels/13.png rename to src/themes/default/assets/img/levels/13.png diff --git a/http/czt/img/levels/14.png b/src/themes/default/assets/img/levels/14.png similarity index 100% rename from http/czt/img/levels/14.png rename to src/themes/default/assets/img/levels/14.png diff --git a/http/czt/img/levels/15.png b/src/themes/default/assets/img/levels/15.png similarity index 100% rename from http/czt/img/levels/15.png rename to src/themes/default/assets/img/levels/15.png diff --git a/http/czt/img/levels/16.png b/src/themes/default/assets/img/levels/16.png similarity index 100% rename from http/czt/img/levels/16.png rename to src/themes/default/assets/img/levels/16.png diff --git a/http/czt/img/levels/17.png b/src/themes/default/assets/img/levels/17.png similarity index 100% rename from http/czt/img/levels/17.png rename to src/themes/default/assets/img/levels/17.png diff --git a/http/czt/img/levels/18.png b/src/themes/default/assets/img/levels/18.png similarity index 100% rename from http/czt/img/levels/18.png rename to src/themes/default/assets/img/levels/18.png diff --git a/http/czt/img/levels/19.png b/src/themes/default/assets/img/levels/19.png similarity index 100% rename from http/czt/img/levels/19.png rename to src/themes/default/assets/img/levels/19.png diff --git a/http/czt/img/levels/2.png b/src/themes/default/assets/img/levels/2.png similarity index 100% rename from http/czt/img/levels/2.png rename to src/themes/default/assets/img/levels/2.png diff --git a/http/czt/img/levels/20.png b/src/themes/default/assets/img/levels/20.png similarity index 100% rename from http/czt/img/levels/20.png rename to src/themes/default/assets/img/levels/20.png diff --git a/http/czt/img/levels/21.png b/src/themes/default/assets/img/levels/21.png similarity index 100% rename from http/czt/img/levels/21.png rename to src/themes/default/assets/img/levels/21.png diff --git a/http/czt/img/levels/22.png b/src/themes/default/assets/img/levels/22.png similarity index 100% rename from http/czt/img/levels/22.png rename to src/themes/default/assets/img/levels/22.png diff --git a/http/czt/img/levels/23.png b/src/themes/default/assets/img/levels/23.png similarity index 100% rename from http/czt/img/levels/23.png rename to src/themes/default/assets/img/levels/23.png diff --git a/http/czt/img/levels/24.png b/src/themes/default/assets/img/levels/24.png similarity index 100% rename from http/czt/img/levels/24.png rename to src/themes/default/assets/img/levels/24.png diff --git a/http/czt/img/levels/25.png b/src/themes/default/assets/img/levels/25.png similarity index 100% rename from http/czt/img/levels/25.png rename to src/themes/default/assets/img/levels/25.png diff --git a/http/czt/img/levels/26.png b/src/themes/default/assets/img/levels/26.png similarity index 100% rename from http/czt/img/levels/26.png rename to src/themes/default/assets/img/levels/26.png diff --git a/http/czt/img/levels/27.png b/src/themes/default/assets/img/levels/27.png similarity index 100% rename from http/czt/img/levels/27.png rename to src/themes/default/assets/img/levels/27.png diff --git a/http/czt/img/levels/28.png b/src/themes/default/assets/img/levels/28.png similarity index 100% rename from http/czt/img/levels/28.png rename to src/themes/default/assets/img/levels/28.png diff --git a/http/czt/img/levels/29.png b/src/themes/default/assets/img/levels/29.png similarity index 100% rename from http/czt/img/levels/29.png rename to src/themes/default/assets/img/levels/29.png diff --git a/http/czt/img/levels/3.png b/src/themes/default/assets/img/levels/3.png similarity index 100% rename from http/czt/img/levels/3.png rename to src/themes/default/assets/img/levels/3.png diff --git a/http/czt/img/levels/30.png b/src/themes/default/assets/img/levels/30.png similarity index 100% rename from http/czt/img/levels/30.png rename to src/themes/default/assets/img/levels/30.png diff --git a/http/czt/img/levels/31.png b/src/themes/default/assets/img/levels/31.png similarity index 100% rename from http/czt/img/levels/31.png rename to src/themes/default/assets/img/levels/31.png diff --git a/http/czt/img/levels/32.png b/src/themes/default/assets/img/levels/32.png similarity index 100% rename from http/czt/img/levels/32.png rename to src/themes/default/assets/img/levels/32.png diff --git a/http/czt/img/levels/33.png b/src/themes/default/assets/img/levels/33.png similarity index 100% rename from http/czt/img/levels/33.png rename to src/themes/default/assets/img/levels/33.png diff --git a/http/czt/img/levels/34.png b/src/themes/default/assets/img/levels/34.png similarity index 100% rename from http/czt/img/levels/34.png rename to src/themes/default/assets/img/levels/34.png diff --git a/http/czt/img/levels/35.png b/src/themes/default/assets/img/levels/35.png similarity index 100% rename from http/czt/img/levels/35.png rename to src/themes/default/assets/img/levels/35.png diff --git a/http/czt/img/levels/36.png b/src/themes/default/assets/img/levels/36.png similarity index 100% rename from http/czt/img/levels/36.png rename to src/themes/default/assets/img/levels/36.png diff --git a/http/czt/img/levels/37.png b/src/themes/default/assets/img/levels/37.png similarity index 100% rename from http/czt/img/levels/37.png rename to src/themes/default/assets/img/levels/37.png diff --git a/http/czt/img/levels/38.png b/src/themes/default/assets/img/levels/38.png similarity index 100% rename from http/czt/img/levels/38.png rename to src/themes/default/assets/img/levels/38.png diff --git a/http/czt/img/levels/39.png b/src/themes/default/assets/img/levels/39.png similarity index 100% rename from http/czt/img/levels/39.png rename to src/themes/default/assets/img/levels/39.png diff --git a/http/czt/img/levels/4.png b/src/themes/default/assets/img/levels/4.png similarity index 100% rename from http/czt/img/levels/4.png rename to src/themes/default/assets/img/levels/4.png diff --git a/http/czt/img/levels/40.png b/src/themes/default/assets/img/levels/40.png similarity index 100% rename from http/czt/img/levels/40.png rename to src/themes/default/assets/img/levels/40.png diff --git a/http/czt/img/levels/41.png b/src/themes/default/assets/img/levels/41.png similarity index 100% rename from http/czt/img/levels/41.png rename to src/themes/default/assets/img/levels/41.png diff --git a/http/czt/img/levels/42.png b/src/themes/default/assets/img/levels/42.png similarity index 100% rename from http/czt/img/levels/42.png rename to src/themes/default/assets/img/levels/42.png diff --git a/http/czt/img/levels/43.png b/src/themes/default/assets/img/levels/43.png similarity index 100% rename from http/czt/img/levels/43.png rename to src/themes/default/assets/img/levels/43.png diff --git a/http/czt/img/levels/5.png b/src/themes/default/assets/img/levels/5.png similarity index 100% rename from http/czt/img/levels/5.png rename to src/themes/default/assets/img/levels/5.png diff --git a/http/czt/img/levels/6.png b/src/themes/default/assets/img/levels/6.png similarity index 100% rename from http/czt/img/levels/6.png rename to src/themes/default/assets/img/levels/6.png diff --git a/http/czt/img/levels/7.png b/src/themes/default/assets/img/levels/7.png similarity index 100% rename from http/czt/img/levels/7.png rename to src/themes/default/assets/img/levels/7.png diff --git a/http/czt/img/levels/8.png b/src/themes/default/assets/img/levels/8.png similarity index 100% rename from http/czt/img/levels/8.png rename to src/themes/default/assets/img/levels/8.png diff --git a/http/czt/img/levels/9.png b/src/themes/default/assets/img/levels/9.png similarity index 100% rename from http/czt/img/levels/9.png rename to src/themes/default/assets/img/levels/9.png diff --git a/http/czt/img/levels/cube.png b/src/themes/default/assets/img/levels/cube.png similarity index 100% rename from http/czt/img/levels/cube.png rename to src/themes/default/assets/img/levels/cube.png diff --git a/http/czt_timed/img/sources/cybergame.png b/src/themes/default/assets/img/sources/cybergame.png similarity index 100% rename from http/czt_timed/img/sources/cybergame.png rename to src/themes/default/assets/img/sources/cybergame.png diff --git a/http/czt_timed/img/sources/empire.png b/src/themes/default/assets/img/sources/empire.png similarity index 100% rename from http/czt_timed/img/sources/empire.png rename to src/themes/default/assets/img/sources/empire.png diff --git a/http/czt/img/sources/fs.png b/src/themes/default/assets/img/sources/fs.png similarity index 100% rename from http/czt/img/sources/fs.png rename to src/themes/default/assets/img/sources/fs.png diff --git a/http/czt_timed/img/sources/gamerstv.png b/src/themes/default/assets/img/sources/gamerstv.png similarity index 100% rename from http/czt_timed/img/sources/gamerstv.png rename to src/themes/default/assets/img/sources/gamerstv.png diff --git a/http/czt_timed/img/sources/gg.png b/src/themes/default/assets/img/sources/gg.png similarity index 100% rename from http/czt_timed/img/sources/gg.png rename to src/themes/default/assets/img/sources/gg.png diff --git a/http/czt_timed/img/sources/gipsyteam.png b/src/themes/default/assets/img/sources/gipsyteam.png similarity index 100% rename from http/czt_timed/img/sources/gipsyteam.png rename to src/themes/default/assets/img/sources/gipsyteam.png diff --git a/http/czt_timed/img/sources/gohatv.png b/src/themes/default/assets/img/sources/gohatv.png similarity index 100% rename from http/czt_timed/img/sources/gohatv.png rename to src/themes/default/assets/img/sources/gohatv.png diff --git a/http/czt_timed/img/sources/hitboxtv.png b/src/themes/default/assets/img/sources/hitboxtv.png similarity index 100% rename from http/czt_timed/img/sources/hitboxtv.png rename to src/themes/default/assets/img/sources/hitboxtv.png diff --git a/http/czt_timed/img/sources/lalka_cup.png b/src/themes/default/assets/img/sources/lalka_cup.png similarity index 100% rename from http/czt_timed/img/sources/lalka_cup.png rename to src/themes/default/assets/img/sources/lalka_cup.png diff --git a/http/czt_timed/img/sources/midlane.png b/src/themes/default/assets/img/sources/midlane.png similarity index 100% rename from http/czt_timed/img/sources/midlane.png rename to src/themes/default/assets/img/sources/midlane.png diff --git a/http/czt_timed/img/sources/streamcube.png b/src/themes/default/assets/img/sources/streamcube.png similarity index 100% rename from http/czt_timed/img/sources/streamcube.png rename to src/themes/default/assets/img/sources/streamcube.png diff --git a/http/czt_timed/img/sources/tw.png b/src/themes/default/assets/img/sources/tw.png similarity index 100% rename from http/czt_timed/img/sources/tw.png rename to src/themes/default/assets/img/sources/tw.png diff --git a/http/czt_timed/img/sources/youtube.png b/src/themes/default/assets/img/sources/youtube.png similarity index 100% rename from http/czt_timed/img/sources/youtube.png rename to src/themes/default/assets/img/sources/youtube.png diff --git a/src/themes/default/assets/index.html b/src/themes/default/assets/index.html new file mode 100644 index 0000000..d23d397 --- /dev/null +++ b/src/themes/default/assets/index.html @@ -0,0 +1,38 @@ + + + + Lalka - chat + + + + + + + +
+
+ +
+ +
+
+
+ +
+ + + +
+ +
+ +
{{message.display_name || message.user}}:
+
+
+
+ + \ No newline at end of file diff --git a/http/czt/levels.xml b/src/themes/default/assets/levels.xml similarity index 100% rename from http/czt/levels.xml rename to src/themes/default/assets/levels.xml diff --git a/src/themes/default/assets/settings.json b/src/themes/default/assets/settings.json new file mode 100644 index 0000000..18fde22 --- /dev/null +++ b/src/themes/default/assets/settings.json @@ -0,0 +1,10 @@ +{ + "show_system_msg": true, + "remove_message": true, + "remove_text": "", + "timer": "-1", + "message_opacity": "67", + "font_size": "15", + "smile_size": "20", + "badge_size": "16" +} \ No newline at end of file diff --git a/src/themes/default/assets/settings_gui.json b/src/themes/default/assets/settings_gui.json new file mode 100644 index 0000000..db33650 --- /dev/null +++ b/src/themes/default/assets/settings_gui.json @@ -0,0 +1,27 @@ +{ + "font_size": { + "view": "spin", + "min": 10, + "max": 100 + }, + "timer": { + "view": "spin", + "min": -1, + "max": 3600 + }, + "smile_size": { + "view": "spin", + "min": 8, + "max": 128 + }, + "message_opacity": { + "view": "spin", + "min": 0, + "max": 100 + }, + "badge_size": { + "view": "spin", + "min": 8, + "max": 128 + } +} diff --git a/src/themes/default/config/webpack.common.js b/src/themes/default/config/webpack.common.js new file mode 100644 index 0000000..8b38fd3 --- /dev/null +++ b/src/themes/default/config/webpack.common.js @@ -0,0 +1,38 @@ +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: { + app: './app/app' + }, + + resolve: { + extensions: ['', '.ts', '.js', '.json'], + modulesDirectories: ['./node_modules', './app'], + alias: { + 'vue$': 'vue/dist/vue.common.js' + } + }, + + output: { + path: './dist' + }, + + module: { + loaders: [ + + ] + }, + + plugins: [ + new CopyWebpackPlugin([ + { from: './assets' } + ]), + + new HtmlWebpackPlugin({ + template: './assets/index.html', + chunksSortMode: 'dependency', + inject: 'body' + }) + ] +}; \ No newline at end of file diff --git a/src/themes/default/config/webpack.development.js b/src/themes/default/config/webpack.development.js new file mode 100644 index 0000000..6d5fb9d --- /dev/null +++ b/src/themes/default/config/webpack.development.js @@ -0,0 +1,10 @@ +const merge = require('webpack-merge'); +const common = require('./webpack.common'); + +const development = { + output: { + filename: './js/[name].js' + } +}; + +module.exports = merge(common, development); diff --git a/src/themes/default/config/webpack.production.js b/src/themes/default/config/webpack.production.js new file mode 100644 index 0000000..cfa8f5d --- /dev/null +++ b/src/themes/default/config/webpack.production.js @@ -0,0 +1,28 @@ +const merge = require('webpack-merge'); +const common = require('./webpack.common'); + +const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); +const NoErrorsPlugin = require('webpack/lib/NoErrorsPlugin'); +const DefinePlugin = require('webpack/lib/DefinePlugin'); + +const production = { + devtool: null, + + output: { + filename: './js/[name].min.js' + }, + + plugins: [ + new NoErrorsPlugin(), + + new DefinePlugin({ + 'process.env': { + NODE_ENV: '"production"' + } + }), + + new UglifyJsPlugin() + ] +}; + +module.exports = merge(common, production); diff --git a/src/themes/default/package.json b/src/themes/default/package.json new file mode 100644 index 0000000..0c42c86 --- /dev/null +++ b/src/themes/default/package.json @@ -0,0 +1,21 @@ +{ + "name": "lc-default-theme", + "version": "0.3.5", + "repository": "https://github.com/DeForce/LalkaChat", + "license": "GPLv3", + "dependencies": { + "dompurify": "^0.8.4", + "vue": "^2.1.8" + }, + "devDependencies": { + "copy-webpack-plugin": "^4.0.1", + "html-webpack-plugin": "^2.25.0", + "webpack": "^1.14.0", + "webpack-merge": "^2.0.0" + }, + "scripts": { + "start": "webpack --config config/webpack.production.js", + "build:dev": "webpack --config config/webpack.development.js", + "build:prod": "webpack --config config/webpack.production.js" + } +} diff --git a/translations/en/goodgame.key b/translations/en/goodgame.key index f5d34e9..ae91817 100644 --- a/translations/en/goodgame.key +++ b/translations/en/goodgame.key @@ -1,4 +1,11 @@ goodgame = GoodGame goodgame.config = Settings goodgame.config.socket = WebSocket parameters -goodgame.config.channel_name = Channel name \ No newline at end of file +goodgame.config.channel_name = Channel name +goodgame.connection_success = Connection Successful +goodgame.connection_died = Connection died, trying to reconnect +goodgame.join_success = Successfully joined channel {0} +goodgame.warning = {0} warned {1} +goodgame.ban = {0} banned {1} for {2} minutes because of: {3} +goodgame.ban_permanent = {1} was banned permanently by {0} +goodgame.unban = {1} was unbanned by {0} diff --git a/translations/en/main.key b/translations/en/main.key index 5f3ff33..e9226e5 100644 --- a/translations/en/main.key +++ b/translations/en/main.key @@ -2,10 +2,12 @@ menu.settings = Settings menu.reload = Reload WebChat *.apply_button = Apply *.cancel_button = Cancel +*.ok_button = OK *.list_add = Add *.list_remove = Remove *.descr_explain = Click on an item to view description of the item *.description = No description +*.show_pm = Mark messages for channel settings = Settings settings.main = Main Settings @@ -16,6 +18,8 @@ main = Main main.gui = GUI Settings main.gui.show_hidden = Show hidden items main.gui.gui = Is GUI enabled +main.gui.show_browser = Show browser window +main.gui.show_counters = Show channel view counters main.gui.on_top = Show window on top main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Just a big parameter for test main.gui.reload = Reload WebChat diff --git a/translations/en/sc2tv.key b/translations/en/sc2tv.key index ba80a33..1d33edb 100644 --- a/translations/en/sc2tv.key +++ b/translations/en/sc2tv.key @@ -1,4 +1,8 @@ sc2tv = sc2tv sc2tv.config = Settings sc2tv.config.socket = WebSocket parameters -sc2tv.config.channel_name = Channel name \ No newline at end of file +sc2tv.config.channel_name = Channel name +sc2tv.connection_success = Connection Successful +sc2tv.connection_died = Connection died, trying to reconnect +sc2tv.join_success = Joined channel {0} +sc2tv.joining = Joining channel {0}... \ No newline at end of file diff --git a/translations/en/twitch.key b/translations/en/twitch.key index 0e2f058..e2fb4d1 100644 --- a/translations/en/twitch.key +++ b/translations/en/twitch.key @@ -3,4 +3,8 @@ twitch.config = Settings twitch.config.host = IRC hostname twitch.config.port = IRC port twitch.config.channel = Channel name -twitch.config.bttv = Show BTTV smiles \ No newline at end of file +twitch.config.bttv = Show BTTV smiles +twitch.connection_success = Connection Successful +twitch.connection_died = Connection died, trying to reconnect +twitch.join_success = Joined channel {0} +twitch.joining = Joining channel {0}... diff --git a/translations/en/webchat.key b/translations/en/webchat.key index 810c058..faf2327 100644 --- a/translations/en/webchat.key +++ b/translations/en/webchat.key @@ -7,4 +7,11 @@ webchat.server.port = Port webchat.style = Available styles webchat.style.list_box = webchat.style_settings = Style Settings -webchat.style_settings.font_size = Font Size (pt) \ No newline at end of file +webchat.style_settings.font_size = Font Size (pt) +webchat.style_settings.show_system_msg = Show system messages +webchat.style_settings.remove_message = Replace deleted Messages +webchat.style_settings.remove_text = Replaced Message text +webchat.style_settings.timer = Message clear timer +webchat.style_settings.message_opacity = Background opacity +webchat.style_settings.smile_size = Smile Size (px) +webchat.style_settings.badge_size = Badge Size (px) \ No newline at end of file diff --git a/translations/ru/goodgame.key b/translations/ru/goodgame.key index 3d4ebc7..d4714cc 100644 --- a/translations/ru/goodgame.key +++ b/translations/ru/goodgame.key @@ -1,4 +1,11 @@ goodgame = GoodGame goodgame.config = Настройки goodgame.config.socket = Настройки WebSocket -goodgame.config.channel_name = Название канала \ No newline at end of file +goodgame.config.channel_name = Название канала +goodgame.connection_success = Соединение установлено +goodgame.connection_died = Соединение было прервано, попытка переподключения... +goodgame.join_success = Подключение к каналу {0} успешно +goodgame.warning = {0} вынес предупреждение {1} +goodgame.ban = {0} забанил {1} на {2} минут по причине: {3} +goodgame.ban_permanent = {0} забанил бессрочно {1} +goodgame.unban = {0} разбанил {1} diff --git a/translations/ru/main.key b/translations/ru/main.key index 39c5fc2..78619d7 100644 --- a/translations/ru/main.key +++ b/translations/ru/main.key @@ -2,10 +2,12 @@ menu.settings = Настройки menu.reload = Перезагрузить Чат *.apply_button = Принять *.cancel_button = Отменить +*.ok_button = ОК *.list_add = Добавить *.list_remove = Удалить *.descr_explain = Выберите вещь, описание которой вы хотите прочитать. *.description = Описание не предоставлено +*.show_pm = Выделять личные сообщения settings = Настройки settings.main = Главные Настройки @@ -16,6 +18,8 @@ main = Главные Настройки main.gui = Настройки Интерфейса main.gui.show_hidden = Показвать скрытые вещи main.gui.gui = Интерфейс Включен +main.gui.show_browser = Показывать окно браузера +main.gui.show_counters = Показывать счетчики зрителей main.gui.on_top = Окно поверх всех main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Тестовый Параметр main.gui.reload = Перезагрузить ВебЧат diff --git a/translations/ru/sc2tv.key b/translations/ru/sc2tv.key index 75a820c..e9e02ad 100644 --- a/translations/ru/sc2tv.key +++ b/translations/ru/sc2tv.key @@ -1,4 +1,8 @@ sc2tv = sc2tv sc2tv.config = Настройки sc2tv.config.socket = WS параметры -sc2tv.config.channel_name = Название канала \ No newline at end of file +sc2tv.config.channel_name = Название канала +sc2tv.connection_success = Соединение Установлено +sc2tv.connection_died = Соединение было прервано, попытка переподключения... +sc2tv.join_success = Подключение к каналу {0} успешно +sc2tv.joining = Подключение к каналу {0}... diff --git a/translations/ru/twitch.key b/translations/ru/twitch.key index a966bea..42c9013 100644 --- a/translations/ru/twitch.key +++ b/translations/ru/twitch.key @@ -3,4 +3,8 @@ twitch.config = Настройки twitch.config.host = IRC хостнейм twitch.config.port = IRC порт twitch.config.channel = Имя канала -twitch.config.bttv = Показать BTTV смайлы \ No newline at end of file +twitch.config.bttv = Показать BTTV смайлы +twitch.connection_success = Соединение Установлено +twitch.connection_died = Соединение было прервано, попытка переподключения... +twitch.join_success = Подключение к каналу {0} успешно +twitch.joining = Подключение к каналу {0}... diff --git a/translations/ru/webchat.key b/translations/ru/webchat.key index 14e5dd4..34065c0 100644 --- a/translations/ru/webchat.key +++ b/translations/ru/webchat.key @@ -8,4 +8,10 @@ webchat.style = Доступные Стили webchat.style.list_box = webchat.style_settings = Настройки Стиля webchat.style_settings.font_size = Размер шрифта - +webchat.style_settings.show_system_msg = Показывать системные сообщения +webchat.style_settings.remove_message = Заменять удалённые сообщения +webchat.style_settings.remove_text = Текст замещенного сообщения +webchat.style_settings.timer = Срок жизни сообщения +webchat.style_settings.message_opacity = Прозрачность фона +webchat.style_settings.smile_size = Размер смайлов (px) +webchat.style_settings.badge_size = Размер бейджей (px) diff --git a/travis/main.sh b/travis/main.sh new file mode 100755 index 0000000..042b1ac --- /dev/null +++ b/travis/main.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +if [ $USER != "travis" ] +then + echo "This script is not designed to run locally." + exit 1 +fi + +case "$TEST_SUITE" in + 'runtime') + export DISPLAY=':99.0' + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + # TODO: Create DockerHub autobuild ============================================================================== + printf "BUILDING_LALKACHAT_BUILD_DEPS" + printf '=%.0s' {1..71} + echo + docker pull deforce/alpine-wxpython:latest + docker build -t deforce/lalkachat-build-deps:latest "${TRAVIS_BUILD_DIR}/docker/Dockerfiles/lalkachat-build-deps" + if [ $? -ne 0 ] + then + echo "\"docker build -t deforce/lalkachat-build-deps:latest ...\" failed!" + exit 1 + fi + # =============================================================================================================== + printf "BUILDING_LALKACHAT" + printf '=%.0s' {1..82} + echo + # docker pull lalkachat-build-deps:latest + docker build -t deforce/lalkachat:testing -f Dockerfile_test "${TRAVIS_BUILD_DIR}" + if [ $? -ne 0 ] + then + echo "\"docker build -t deforce/lalkachat:testing ...\" failed!" + exit 1 + fi + printf "TESTING" + printf '=%.0s' {1..93} + echo + xhost + > /dev/null 2>&1 + id=$(docker run -d -t -v "${TRAVIS_BUILD_DIR}/travis/test-conf:/usr/lib/python2.7/site-packages/LalkaChat/conf" -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY --net=host deforce/lalkachat:testing) + sleep 10 + printf "LALKA_LOGS" + printf '=%.0s' {1..90} + echo + # We can add logs assertions here + docker logs $id + printf '=%.0s' {1..110} + echo + echo "Chat is alive ?" + if ! docker top $id &>/dev/null + then + echo "Czt kills kittens..." + exit 1 + fi + echo "Yeah!" + echo "127.0.0.1:8080 is up and its response contains 'Lalka - chat' ?" + if curl -s "127.0.0.1:8080" | grep "Lalka - chat" + then + echo "Great success!" + else + echo "127.0.0.1:8080 where is Lalka - chat?" + exit 1 + fi + printf '=%.0s' {1..100} + echo + echo "phantomjs test goes here" + echo $(phantomjs --version) + printf '=%.0s' {1..100} + echo + exit 0 + ;; + 'pylint') + docker pull eeacms/pylint:latest + docker run -v "${TRAVIS_BUILD_DIR}":/code eeacms/pylint:latest --max-line-length=99 --output-format=colorized /code + exit $? + ;; + 'jslint') + docker pull eeacms/csslint:latest + docker run -v "${TRAVIS_BUILD_DIR}/http":/code eeacms/jslint:latest --color /code/**/*.js + exit $? + ;; + 'csslint') + docker pull eeacms/jslint:latest + docker run -v "${TRAVIS_BUILD_DIR}":/code eeacms/csslint:latest --format=text /code + exit $? + ;; + *) + echo "TEST_SUITE env variable specifies unknown test suite: \"${TEST_SUITE}\"" + exit 1 + ;; +esac diff --git a/travis/test-conf/blacklist.cfg b/travis/test-conf/blacklist.cfg new file mode 100644 index 0000000..3ed5838 --- /dev/null +++ b/travis/test-conf/blacklist.cfg @@ -0,0 +1,15 @@ +[gui_information] +category = messaging +id = 30 + +[main] +message = ignored message + +[users_hide] + +[users_block] + +[words_hide] + +[words_block] + diff --git a/travis/test-conf/c2b.cfg b/travis/test-conf/c2b.cfg new file mode 100644 index 0000000..c3dfe10 --- /dev/null +++ b/travis/test-conf/c2b.cfg @@ -0,0 +1,6 @@ +[gui_information] +category = messaging +id = 10 + +[config] + diff --git a/travis/test-conf/chat_modules.cfg b/travis/test-conf/chat_modules.cfg new file mode 100644 index 0000000..45f4ba3 --- /dev/null +++ b/travis/test-conf/chat_modules.cfg @@ -0,0 +1,8 @@ +[gui_information] +category = chat + +[chats] +twitch +goodgame +sc2tv + diff --git a/travis/test-conf/config.cfg b/travis/test-conf/config.cfg new file mode 100644 index 0000000..a244b32 --- /dev/null +++ b/travis/test-conf/config.cfg @@ -0,0 +1,15 @@ +[gui] +show_browser = False +show_hidden = False +gui = True +on_top = True +reload + +[gui_information] +category = main +width = 1271 +height = 539 + +[language] +en + diff --git a/travis/test-conf/df.cfg b/travis/test-conf/df.cfg new file mode 100644 index 0000000..c4be0ea --- /dev/null +++ b/travis/test-conf/df.cfg @@ -0,0 +1,9 @@ +[gui_information] +category = messaging + +[grep] +symbol = # +file = logs/df.txt + +[prof] + diff --git a/travis/test-conf/goodgame.cfg b/travis/test-conf/goodgame.cfg new file mode 100644 index 0000000..e44fbf6 --- /dev/null +++ b/travis/test-conf/goodgame.cfg @@ -0,0 +1,7 @@ +[gui_information] +category = chat + +[config] +channel_name = CzT +socket = ws://chat.goodgame.ru:8081/chat/websocket + diff --git a/travis/test-conf/levels.cfg b/travis/test-conf/levels.cfg new file mode 100644 index 0000000..ebe3fe5 --- /dev/null +++ b/travis/test-conf/levels.cfg @@ -0,0 +1,11 @@ +[gui_information] +category = messaging + +[config] +message = {0} has leveled up, now he is {1} +db = conf/levels.db +experience = geometrical +exp_for_level = 200 +exp_for_message = 1 +decrease_window = 60 + diff --git a/travis/test-conf/logger.cfg b/travis/test-conf/logger.cfg new file mode 100644 index 0000000..db38446 --- /dev/null +++ b/travis/test-conf/logger.cfg @@ -0,0 +1,10 @@ +[gui_information] +category = messaging +id = 20 + +[config] +logging = True +file_format = %Y-%m-%d +message_date_format = %Y-%m-%d %H:%M:%S +rotation = daily + diff --git a/travis/test-conf/mentions.cfg b/travis/test-conf/mentions.cfg new file mode 100644 index 0000000..0e7ea10 --- /dev/null +++ b/travis/test-conf/mentions.cfg @@ -0,0 +1,7 @@ +[gui_information] +category = messaging + +[mentions] + +[address] + diff --git a/travis/test-conf/messaging_modules.cfg b/travis/test-conf/messaging_modules.cfg new file mode 100644 index 0000000..2b0b1a9 --- /dev/null +++ b/travis/test-conf/messaging_modules.cfg @@ -0,0 +1,12 @@ +[gui_information] +category = messaging + +[messaging] +webchat +mentions +df +logger +blacklist +levels +c2b + diff --git a/travis/test-conf/sc2tv.cfg b/travis/test-conf/sc2tv.cfg new file mode 100644 index 0000000..cac8723 --- /dev/null +++ b/travis/test-conf/sc2tv.cfg @@ -0,0 +1,7 @@ +[gui_information] +category = chat + +[config] +channel_name = czt +socket = ws://funstream.tv/socket.io/ + diff --git a/travis/test-conf/twitch.cfg b/travis/test-conf/twitch.cfg new file mode 100644 index 0000000..de9c4e2 --- /dev/null +++ b/travis/test-conf/twitch.cfg @@ -0,0 +1,9 @@ +[gui_information] +category = chat + +[config] +channel = czt1 +bttv = True +host = irc.twitch.tv +port = 6667 + diff --git a/travis/test-conf/webchat.cfg b/travis/test-conf/webchat.cfg new file mode 100644 index 0000000..d0f416f --- /dev/null +++ b/travis/test-conf/webchat.cfg @@ -0,0 +1,16 @@ +[server] +host = 0.0.0.0 +port = 8080 + +[gui_information] +category = main +id = 9001 + +[style] +czt + +[style_settings] +font_size = 15 +show_system_msg = False +timer = -1 +