From ed8ae4608678c2f911c262b160a194354422c9e4 Mon Sep 17 00:00:00 2001 From: Mindiell Date: Thu, 11 Jan 2024 08:17:25 +0100 Subject: [PATCH] First commit --- .gitignore | 8 + LICENSE | 661 ++++++++++++++++++ README.md | 3 + bot/__init__.py | 0 bot/hebdobot.py | 53 ++ bot/hooks/__init__.py | 51 ++ bot/hooks/anniv.py | 47 ++ bot/hooks/bad_command.py | 13 + bot/hooks/cancel_previous_input.py | 30 + bot/hooks/chrono.py | 20 + bot/hooks/collective_subject.py | 46 ++ bot/hooks/comment.py | 12 + bot/hooks/current.py | 31 + bot/hooks/date.py | 17 + bot/hooks/default.py | 11 + bot/hooks/finish_review.py | 99 +++ bot/hooks/hello.py | 21 + bot/hooks/help.py | 44 ++ bot/hooks/individual_subject.py | 45 ++ bot/hooks/input_review.py | 13 + bot/hooks/license.py | 17 + bot/hooks/listen_alexandrie.py | 12 + bot/hooks/missing.py | 37 + bot/hooks/record.py | 26 + bot/hooks/start_review.py | 30 + bot/hooks/stats.py | 48 ++ bot/hooks/status.py | 31 + bot/hooks/stop_review.py | 28 + bot/hooks/thanks.py | 15 + bot/hooks/version.py | 13 + bot/review/__init__.py | 0 bot/review/aliases.py | 31 + bot/review/review.py | 250 +++++++ bot/review/stats.py | 177 +++++ bot/review/topic.py | 60 ++ hebdobot.py | 24 + ircbot.py | 54 ++ logger.py | 29 + requirements-dev.txt | 4 + requirements.txt | 2 + settings.py | 34 + tests/__init__.py | 0 .../20231201-log-irc-revue-hebdomadaire.txt | 445 ++++++++++++ tests/datas/irc.txt | 152 ++++ tests/datas/reviewstats.csv | 313 +++++++++ tests/settings.py | 34 + tests/test_aliases.py | 9 + tests/test_basic_hooks.py | 124 ++++ tests/test_bot.py | 12 + tests/test_complete_review.py | 31 + tests/test_review_hooks.py | 507 ++++++++++++++ tests/test_topic.py | 8 + tests/users.conf | 10 + tests/utils.py | 13 + 54 files changed, 3805 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bot/__init__.py create mode 100644 bot/hebdobot.py create mode 100644 bot/hooks/__init__.py create mode 100644 bot/hooks/anniv.py create mode 100644 bot/hooks/bad_command.py create mode 100644 bot/hooks/cancel_previous_input.py create mode 100644 bot/hooks/chrono.py create mode 100644 bot/hooks/collective_subject.py create mode 100644 bot/hooks/comment.py create mode 100644 bot/hooks/current.py create mode 100644 bot/hooks/date.py create mode 100644 bot/hooks/default.py create mode 100644 bot/hooks/finish_review.py create mode 100644 bot/hooks/hello.py create mode 100644 bot/hooks/help.py create mode 100644 bot/hooks/individual_subject.py create mode 100644 bot/hooks/input_review.py create mode 100644 bot/hooks/license.py create mode 100644 bot/hooks/listen_alexandrie.py create mode 100644 bot/hooks/missing.py create mode 100644 bot/hooks/record.py create mode 100644 bot/hooks/start_review.py create mode 100644 bot/hooks/stats.py create mode 100644 bot/hooks/status.py create mode 100644 bot/hooks/stop_review.py create mode 100644 bot/hooks/thanks.py create mode 100644 bot/hooks/version.py create mode 100644 bot/review/__init__.py create mode 100644 bot/review/aliases.py create mode 100644 bot/review/review.py create mode 100644 bot/review/stats.py create mode 100644 bot/review/topic.py create mode 100644 hebdobot.py create mode 100644 ircbot.py create mode 100644 logger.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 settings.py create mode 100644 tests/__init__.py create mode 100644 tests/datas/20231201-log-irc-revue-hebdomadaire.txt create mode 100644 tests/datas/irc.txt create mode 100644 tests/datas/reviewstats.csv create mode 100644 tests/settings.py create mode 100644 tests/test_aliases.py create mode 100644 tests/test_basic_hooks.py create mode 100644 tests/test_bot.py create mode 100644 tests/test_complete_review.py create mode 100644 tests/test_review_hooks.py create mode 100644 tests/test_topic.py create mode 100644 tests/users.conf create mode 100644 tests/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77775c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +venv/ +__pycache__/ + +*.log +reviews/ + +.coverage +htmlcov/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbbe355 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..367b6f9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Hebdobot v3 + +Ce projet est une refonte d'Hebdobot pour l'april en python. diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/hebdobot.py b/bot/hebdobot.py new file mode 100644 index 0000000..3382475 --- /dev/null +++ b/bot/hebdobot.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +import time + +from bot.hooks import hooks +from bot.review.review import Review + + +@dataclass +class Answer: + target: str + message: str + + +class Hebdobot: + def __init__(self, settings, channel, nickname="hebdobot"): + self.settings = settings + self.channel = channel + self.nickname = nickname + self.answers = [] + + # Gestion de la revue hebdomadaire + self.review = Review() + + def on_private_message(self, channel, sender, message): + """ + Tous les messages privés sont ignorés. Si l'utilisateur tente une commande, on + lui signale de le faire plutôt sur le canal public. + """ + self.answers = [] + if message[0] == "!": + self.send(sender, "vos commandes dans le salon public") + + return self.answers + + def on_public_message(self, channel, sender, message): + """ + Tous les messages publics sont pris en compte. On utilise les hooks chargés au + démarrage pour cela. + """ + self.answers = [] + if self.review.is_started: + self.review.add_message(sender, message) + + for hook in hooks: + if hook.process(self, channel, sender, message): + break + + return self.answers + + def send(self, target, message): + self.answers.append(Answer(target, message)) + if self.review.is_started and target == self.channel: + self.review.add_message(self.nickname, message) diff --git a/bot/hooks/__init__.py b/bot/hooks/__init__.py new file mode 100644 index 0000000..36bdc30 --- /dev/null +++ b/bot/hooks/__init__.py @@ -0,0 +1,51 @@ +from .anniv import Anniv +from .bad_command import BadCommand +from .cancel_previous_input import CancelPreviousInput +from .collective_subject import CollectiveSubject +from .comment import Comment +from .chrono import Chrono +from .current import Current +from .date import Date +from .default import Default +from .finish_review import FinishReview +from .hello import Hello +from .help import Help +from .individual_subject import IndividualSubject +from .input_review import InputReview +from .license import License +from .listen_alexandrie import ListenAlexandrie +from .missing import Missing +from .record import Record +from .start_review import StartReview +from .stats import Stats +from .status import Status +from .stop_review import StopReview +from .thanks import Thanks +from .version import Version + +hooks = ( + ListenAlexandrie(), + CancelPreviousInput(), + Chrono(), + CollectiveSubject(), + Comment(), + Current(), + FinishReview(), + Help(), + IndividualSubject(), + Missing(), + Record(), + StartReview(), + StopReview(), + Stats(), + Status(), + Anniv(), + Date(), + Hello(), + License(), + Thanks(), + Version(), + BadCommand(), + InputReview(), + Default(), +) diff --git a/bot/hooks/anniv.py b/bot/hooks/anniv.py new file mode 100644 index 0000000..0b9d6dc --- /dev/null +++ b/bot/hooks/anniv.py @@ -0,0 +1,47 @@ +from datetime import date, timedelta +import locale + +import logger + + +class Anniv: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie les dates d'anniversaire de la revue. + """ + if message.lower() == "!anniv": + logger.info("!anniv caught.") + + # Calcul des anniversaires + locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + formatter = "%A %d %B %Y" + today = date.today() + + review_birthdate = date(2010, 4, 30) + review_birthday = review_birthdate.strftime(formatter) + review_age = int((today - review_birthdate).days / 365.2425) + bot.send( + channel, + f"La revue hebdomadaire est née le {review_birthday} et a " + f"{review_age} ans", + ) + + hebdobot_birthdate = date(2011, 9, 9) + hebdobot_birthday = hebdobot_birthdate.strftime(formatter) + hebdobot_age = int((today - hebdobot_birthdate).days / 365.2425) + bot.send( + channel, + f"Hebdobot a géré sa première revue le {hebdobot_birthday} et a " + f"{hebdobot_age} ans", + ) + + april_birthdate = date(1996, 11, 20) + april_birthday = april_birthdate.strftime(formatter) + april_age = int((today - april_birthdate).days / 365.2425) + bot.send( + channel, + f"L'April a été déclarée en préférecture le {april_birthday} et a " + f"{april_age} ans", + ) + + return True diff --git a/bot/hooks/bad_command.py b/bot/hooks/bad_command.py new file mode 100644 index 0000000..3f4d98e --- /dev/null +++ b/bot/hooks/bad_command.py @@ -0,0 +1,13 @@ +import logger + + +class BadCommand: + def process(self, bot, channel, sender, message): + """ + Si une commande inconnue est détectée, le bot renvoie un message abscons. + """ + if message[0] == "!": + logger.info("unknown command caught.") + + bot.send(channel, f"{sender}, Yo !") + return True diff --git a/bot/hooks/cancel_previous_input.py b/bot/hooks/cancel_previous_input.py new file mode 100644 index 0000000..32cbbf3 --- /dev/null +++ b/bot/hooks/cancel_previous_input.py @@ -0,0 +1,30 @@ +import logger + + +class CancelPreviousInput: + def process(self, bot, channel, sender, message): + """ + Si la commande est la bonne, on supprime le dernier message poussé par + l'utilisateur lors de la revue, si possible. + """ + if message.lower() in ("!oups", "!oops", "!cancelprevious"): + logger.info("!oups caught.") + + if not bot.review.is_started: + bot.send(channel, f"{sender}, pas de revue en cours.") + return True + elif not bot.review.current_topic: + bot.send(channel, f"{sender}, pas de sujet en cours.") + return True + else: + previous_message = bot.review.current_topic.cancel_previous(sender) + if previous_message: + bot.send( + channel, + f"{sender}, suppression de votre précédente entrée : " + f"{previous_message}", + ) + return True + else: + bot.send(channel, f"{sender}, vous n'avez pas d'entrée en cours.") + return True diff --git a/bot/hooks/chrono.py b/bot/hooks/chrono.py new file mode 100644 index 0000000..2386334 --- /dev/null +++ b/bot/hooks/chrono.py @@ -0,0 +1,20 @@ +from datetime import datetime +import logger + + +class Chrono: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie le chrono actuel. + """ + if message.lower() == "!chrono": + logger.info("!chrono caught.") + + if bot.review.is_started: + # TODO: Faire comme l'ancien bot + seconds = (datetime.today() - bot.review.start_time).seconds + bot.send(channel, f"{seconds // 60:02d}:{seconds % 60:02d}") + return True + else: + bot.send(channel, "n/a") + return True diff --git a/bot/hooks/collective_subject.py b/bot/hooks/collective_subject.py new file mode 100644 index 0000000..e8db092 --- /dev/null +++ b/bot/hooks/collective_subject.py @@ -0,0 +1,46 @@ +import re + +import logger + + +class CollectiveSubject: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot démarre un nouveau sujet collectif. + """ + if re.match("^\s*##.*$", message): + logger.info("^\s*##.*$ caught.") + + if bot.review.is_started: + if bot.review.is_owner(sender): + # On lance un nouveau sujet collectif + bot.review.new_collective_topic(message.strip()[2:].strip()) + # S'il y avait un ancien sujet, on le signale + if bot.review.last_topic: + bot.review.last_topic.close() + bot.send( + channel, + f"% durée du point {bot.review.last_topic.title} : " + f"{bot.review.last_topic.duration}", + ) + # Et on prévient les participants + bot.send( + channel, + f"% {' '.join(bot.review.participants)}, on va passer à la " + f"suite : {bot.review.current_topic.title}", + ) + # Et on signale le démarrage du sujet + bot.send( + channel, + f"Sujet collectif : {bot.review.current_topic.title}", + ) + bot.send(channel, "% 1 minute max") + bot.send(channel, "% si rien à signaler vous pouvez écrire % ras") + bot.send(channel, "% quand vous avez fini vous le dites par % fini") + else: + bot.send( + channel, + f"{sender}, vous n'êtes pas responsable de la réunion", + ) + + return True diff --git a/bot/hooks/comment.py b/bot/hooks/comment.py new file mode 100644 index 0000000..67729f4 --- /dev/null +++ b/bot/hooks/comment.py @@ -0,0 +1,12 @@ +import logger + + +class Comment: + def process(self, bot, channel, sender, message): + """ + Si le message est un commentaire, le bot l'ignore. + """ + if message.startswith("%"): + logger.info("comment caught.") + + return True diff --git a/bot/hooks/current.py b/bot/hooks/current.py new file mode 100644 index 0000000..400f1b4 --- /dev/null +++ b/bot/hooks/current.py @@ -0,0 +1,31 @@ +import logger + + +class Current: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie le sujet courant, s'il existe. + """ + if message.lower() == "!courant": + logger.info("!courant caught.") + + if not bot.review.is_started: + bot.send(channel, f"{sender}, pas de revue en cours.") + return True + + if not bot.review.current_topic: + bot.send(channel, "% Pas de sujet en cours.") + return True + + if bot.review.current_topic.collective: + bot.send( + channel, + f"% Sujet collectif en cours : {bot.review.current_topic.title}", + ) + return True + else: + bot.send( + channel, + f"% Sujet individuel en cours : {bot.review.current_topic.title}", + ) + return True diff --git a/bot/hooks/date.py b/bot/hooks/date.py new file mode 100644 index 0000000..3b1ed5f --- /dev/null +++ b/bot/hooks/date.py @@ -0,0 +1,17 @@ +from datetime import datetime +import locale + +import logger + + +class Date: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie la date et l'heure actuelle. + """ + if message.lower() in ("!date", "!time", "!now"): + logger.info("!date caught.") + + locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + bot.send(channel, datetime.today().strftime("%A %d %B %Y %Hh%M")) + return True diff --git a/bot/hooks/default.py b/bot/hooks/default.py new file mode 100644 index 0000000..8ae3760 --- /dev/null +++ b/bot/hooks/default.py @@ -0,0 +1,11 @@ +import logger + + +class Default: + def process(self, bot, channel, sender, message): + """ + Le message est pris en compte en ne faisant rien. + """ + logger.info("default hook.") + + return True diff --git a/bot/hooks/finish_review.py b/bot/hooks/finish_review.py new file mode 100644 index 0000000..e22aa5c --- /dev/null +++ b/bot/hooks/finish_review.py @@ -0,0 +1,99 @@ +from datetime import datetime +import os + +import logger +from bot.review.stats import ReviewData, ReviewStats + + +class FinishReview: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot met fin à la revue hebdomadaire. + """ + if message.lower() == "!fin": + logger.info("!fin caught.") + + if not bot.review.is_started: + bot.send(channel, f"{sender}, pas de revue en cours.") + return True + + if not bot.review.is_owner(sender): + bot.send(channel, f"{sender}, vous n'êtes pas responsable de la revue.") + return True + + if bot.review.is_ended: + bot.send(channel, "La revue est déjà finie.") + return True + + if not bot.review.has_participants: + bot.send(channel, "Participation nulle détectée. La revue est ignorée.") + bot.review.cancel() + return True + + # Ok, on clot la revue hebdomadaire + bot.review.current_topic.close() + bot.send( + channel, + f"% durée du point {bot.review.current_topic.title} : " + f"{bot.review.current_topic.duration}", + ) + bot.review.close() + + # Chargement des statistiques des revues + stats = ReviewStats(bot.settings.REVIEW_STATS) + stats.load() + + # On génère le texte de la revue + report = bot.review.report(stats, bot.settings.USER_ALIASES) + + # Mise à jour des statistiques des revues + if bot.review.user_count > 1: + stats.datas.append( + ReviewData( + bot.review.start_time, + bot.review.user_count, + bot.review.duration, + ) + ) + stats.save() + + # On copie ce texte sur un pad + url = "" + + # On sauve le texte dans un fichier + review_path = os.path.join( + bot.settings.REVIEW_DIRECTORY, + f"{bot.review.start_time.strftime('%Y%m%d')}" + "-log-irc-revue-hebdomadaire.txt", + ) + with open(review_path, "w") as file_handle: + file_handle.write(report) + + bot.send( + channel, + f"% C'était la {stats.size}e revue hebdomadaire de l'April, la " + f"{stats.year_review(bot.review.year)}e de l'année {bot.review.year}.", + ) + bot.send(channel, f"% Compte-rendu de la revue : {url}") + bot.send(channel, "% Durée de la revue : {temps_ecoule} minutes") + bot.send(channel, "% Nombre de personnes participantes : {combien}") + bot.send( + channel, + "% La participation moyenne aux revues est de " + f"{stats.avg_users} personnes", + ) + bot.send( + channel, + f"% {' '.join(bot.review.participants)}, pensez à noter votre " + "bénévalo : http://www.april.org/my?action=benevalo", + ) + bot.send( + channel, + f"% {bot.review.owner}, ne pas oublier d'ajouter le compte-rendu " + "de la revue sur https://agir.april.org/issues/135 en utilisant " + "comme nom de fichier {nom_fichier}", + ) + bot.send(channel, "% Fin de la revue hebdomadaire") + bot.send(bot.review.owner, "Revue finie.") + + return True diff --git a/bot/hooks/hello.py b/bot/hooks/hello.py new file mode 100644 index 0000000..67b8467 --- /dev/null +++ b/bot/hooks/hello.py @@ -0,0 +1,21 @@ +import logger + + +class Hello: + def process(self, bot, channel, sender, message): + """ + Le message est pris en compte si l'utilisateur salue le bot. + """ + if message.lower() in ("!salut", "!bonjour", "!hello") or ( + bot.nickname.lower() in message.lower() + and any( + [ + word in message.lower() + for word in ("bonjour", "salut", "hello", "yo", "hey", "hi") + ] + ) + ): + logger.info("!hello caught.") + + bot.send(channel, f"{sender}, bonjour \\o/") + return True diff --git a/bot/hooks/help.py b/bot/hooks/help.py new file mode 100644 index 0000000..0452390 --- /dev/null +++ b/bot/hooks/help.py @@ -0,0 +1,44 @@ +import logger + + +class Help: + def process(self, bot, channel, sender, message): + """ + Le message est pris en compte si l'utilisateur demande de l'aide. + """ + if message.lower() in ("!aide", "!help", "!aide hebdobot", "!help hebdobot"): + logger.info("!help caught.") + + bot.send( + sender, + f"Bienvenue {sender}. Je suis {bot.nickname}, le robot de gestion " + "des revues hebdomadaires de l'April.", + ) + bot.send(sender, "Voici les commandes que je comprends :") + bot.send(sender, " ") + bot.send(sender, " !aide,!help : afficher cette aide") + bot.send( + sender, + " !aide commande : afficher l'aide de la commande !commande" + ) + bot.send(sender, " !début : commencer une nouvelle revue") + bot.send(sender, " % message  : traiter comme un commentaire") + bot.send(sender, " # titre  : démarrer un sujet individuel") + bot.send(sender, " ## titre  : démarrer un sujet collectif") + bot.send( + sender, + " !oups   : annuler la dernière entrée dans un point de " + "revue", + ) + bot.send(sender, " !courant : afficher le sujet en cours") + bot.send(sender, " !fin : terminer la revue en cours") + bot.send(sender, " !stop  : abandonner la revue en cours") + bot.send(sender, " ") + bot.send( + sender, + "Autres commandes : !anniv, !bonjour, !chrono, !date, !hello, " + "!licence, !manquants, !merci, !record, !salut, !stats, !status, " + "!version", + ) + + return True diff --git a/bot/hooks/individual_subject.py b/bot/hooks/individual_subject.py new file mode 100644 index 0000000..1fae22b --- /dev/null +++ b/bot/hooks/individual_subject.py @@ -0,0 +1,45 @@ +import re + +import logger + + +class IndividualSubject: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot démarre un nouveau sujet individuel. + """ + if re.match("^\s*#[^#].*$", message): + logger.info("^\s*#[^#].*$ caught.") + + if bot.review.is_started: + if bot.review.is_owner(sender): + # On lance un nouveau sujet individuel + bot.review.new_individual_topic(message.strip()[1:].strip()) + # S'il y avait un ancien sujet, on le signale + if bot.review.last_topic: + bot.review.last_topic.close() + bot.send( + channel, + f"% durée du point {bot.review.last_topic.title} : " + f"{bot.review.last_topic.duration}", + ) + # Et on prévient les participants + bot.send( + channel, + f"% {' '.join(bot.review.participants)}, on va passer à la " + f"suite : {bot.review.current_topic.title}", + ) + # Et on signale le démarrage du sujet + bot.send( + channel, + f"Sujet individuel : {bot.review.current_topic.title}", + ) + bot.send(channel, "% si rien à signaler vous pouvez écrire % ras") + bot.send(channel, "% quand vous avez fini vous le dites par % fini") + else: + bot.send( + channel, + f"{sender}, vous n'êtes pas responsable de la réunion", + ) + + return True diff --git a/bot/hooks/input_review.py b/bot/hooks/input_review.py new file mode 100644 index 0000000..4abaf0d --- /dev/null +++ b/bot/hooks/input_review.py @@ -0,0 +1,13 @@ +import logger + + +class InputReview: + def process(self, bot, channel, sender, message): + """ + Tout message prononcé pendant la revue est enregistré. + """ + if bot.review.is_started: + logger.info("message during review caught.") + + bot.review.add_input(sender, message) + return True diff --git a/bot/hooks/license.py b/bot/hooks/license.py new file mode 100644 index 0000000..e41666e --- /dev/null +++ b/bot/hooks/license.py @@ -0,0 +1,17 @@ +import logger + + +class License: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie sa licence. + """ + if message.lower() in ("!license", "!licence"): + logger.info("!license caught.") + bot.send( + channel, + "Hebdobot est un logiciel libre de l'April sous licence GNU AGPLv3+, " + "sources : https://forge.april.org/adminsys/hebdobot", + ) + + return True diff --git a/bot/hooks/listen_alexandrie.py b/bot/hooks/listen_alexandrie.py new file mode 100644 index 0000000..8ed40e8 --- /dev/null +++ b/bot/hooks/listen_alexandrie.py @@ -0,0 +1,12 @@ +import logger + + +class ListenAlexandrie: + def process(self, bot, channel, sender, message): + """ + Si le message est envoyé par Alexandrie, on ne réagit pas. + """ + if sender.lower() == "alexandrie": + logger.info("Alexandrie caught.") + + return True diff --git a/bot/hooks/missing.py b/bot/hooks/missing.py new file mode 100644 index 0000000..3e855d7 --- /dev/null +++ b/bot/hooks/missing.py @@ -0,0 +1,37 @@ +import logger + + +class Missing: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie la liste des personnes n'ayant pas + encore participé au sujet en cours. + """ + if message.lower() == "!manquants": + logger.info("!manquants caught.") + + if not bot.review.is_started: + bot.send(channel, f"{sender}, pas de revue en cours.") + return True + + if bot.review.current_topic is None: + bot.send(channel, "% Pas de sujet en cours.") + return True + + all_participants = bot.review.participants + [bot.review.owner] + missing_participants = list( + set(all_participants) - set(bot.review.current_topic.participants) + ) + if missing_participants == []: + bot.send( + channel, + "% Tout le monde s'est exprimé sur le sujet courant \\o/", + ) + else: + bot.send( + channel, + "% Personnes participantes ne s'étant pas exprimées sur le " + f"sujet courant : {', '.join(missing_participants)}", + ) + + return True diff --git a/bot/hooks/record.py b/bot/hooks/record.py new file mode 100644 index 0000000..160d40c --- /dev/null +++ b/bot/hooks/record.py @@ -0,0 +1,26 @@ +import locale + +import logger +from bot.review.stats import ReviewStats + + +class Record: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie le record de participation à la revue. + """ + if message.lower() == "!record": + logger.info("!record caught.") + + locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + formatter = "%A %d %B %Y" + stats = ReviewStats(bot.settings.REVIEW_STATS) + stats.load() + + bot.send( + channel, + f"Le record de participation est de {stats.biggest.user_count} " + f"personnes le {stats.biggest.date.strftime(formatter)}.", + ) + + return True diff --git a/bot/hooks/start_review.py b/bot/hooks/start_review.py new file mode 100644 index 0000000..ca0c3dc --- /dev/null +++ b/bot/hooks/start_review.py @@ -0,0 +1,30 @@ +import logger + + +class StartReview: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot démarre la revue hebdomadaire. + """ + if message.lower() in ("!start", "!debut", "!début"): + logger.info("!start caught.") + + if bot.review.is_started: + bot.send(channel, f"% {sender}, une revue est déjà en cours.") + return True + + # TODO: tester l'heure de démarrage de la revue + + bot.review.start(sender) + bot.send(sender, f"% Bonjour {sender}, vous êtes responsable de réunion.") + bot.send(sender, "% Pour terminer la réunion, tapez !fin") + # ~ bot.checkReviewAnniversary(); + bot.send(channel, "% Début de la réunion hebdomadaire") + bot.send( + channel, + "% rappel : toute ligne commençant par % sera considérée comme un " + "commentaire et non prise en compte dans la synthèse", + ) + bot.send(channel, "% pour connaître le point courant, taper !courant") + + return True diff --git a/bot/hooks/stats.py b/bot/hooks/stats.py new file mode 100644 index 0000000..f1bc650 --- /dev/null +++ b/bot/hooks/stats.py @@ -0,0 +1,48 @@ +import locale + +import logger +from bot.review.stats import ReviewStats + + +class Stats: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie quelques statistiques de la revue. + """ + if message.lower() == "!stats": + logger.info("!stats caught.") + + locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + formatter = "%A %d %B %Y" + stats = ReviewStats(bot.settings.REVIEW_STATS) + stats.load() + + if stats.size > 0: + bot.send( + channel, + f"% Il y a eu {stats.size} revues. La première date " + f"du {stats.first.date.strftime(formatter)}.", + ) + else: + bot.send(channel, "% Il n'y a pas encore eu de revue.") + + bot.send( + channel, + f"% Participation aux revues : min.={stats.min_users}, " + f"moy.={stats.avg_users:.1f}, max={stats.max_users}", + ) + bot.send( + channel, + f"% Durée des revues : min.={stats.min_duration} min, " + f"moy.={stats.avg_duration:.1f} min, max={stats.max_duration} min", + ) + bot.send( + channel, + f"% Tableau du nombre de personnes participantes : {stats.users_board}", + ) + bot.send( + channel, + f"% Tableau des durées (en minutes) : {stats.durations_board}", + ) + + return True diff --git a/bot/hooks/status.py b/bot/hooks/status.py new file mode 100644 index 0000000..ad75380 --- /dev/null +++ b/bot/hooks/status.py @@ -0,0 +1,31 @@ +import logger + + +class Status: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie son statut actuel à l'utilisateur. + """ + if message.lower() in ("!statut", "!status"): + logger.info("!status caught.") + + bot.send(sender, f"{sender}, voici l'état d'Hebdobot :") + bot.send(sender, f" revue en cours : {bot.review is not None}") + if bot.review.is_started: + bot.send(sender, f" animateur revue : {bot.review.owner}") + else: + bot.send(sender, " animateur revue : none") + # TODO: finish this part + # ~ bot.send_multiple( + # ~ sender, + # ~ ( + # ~ f" Alias settings : {len(bot.alias)}", + # ~ f" Identica settings : {}", + # ~ f" Privatebin settings : {bot.review.pastebin}", + # ~ f" Mastodon settings : {}", + # ~ f" Cron settings : {len(bot.cron.settings)}" + # ~ f" Review Wait Time : {bot.review_wait}", + # ~ ), + # ~ ) + + return True diff --git a/bot/hooks/stop_review.py b/bot/hooks/stop_review.py new file mode 100644 index 0000000..ef448f6 --- /dev/null +++ b/bot/hooks/stop_review.py @@ -0,0 +1,28 @@ +import logger + + +class StopReview: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot abandonne la revue hebdomadaire en cours. + """ + if message.lower() in ("!stop"): + logger.info("!stop caught.") + + if not bot.review.is_started: + bot.send( + channel, + f"{sender}, pas de revue en cours, abandon impossible.", + ) + return True + + if not bot.review.is_owner(sender): + bot.send( + channel, + f"{sender}, vous n'êtes pas responsable de la réunion.", + ) + return True + + bot.send(channel, "Abandon de la revue en cours.") + bot.review.cancel() + return True diff --git a/bot/hooks/thanks.py b/bot/hooks/thanks.py new file mode 100644 index 0000000..6c794d8 --- /dev/null +++ b/bot/hooks/thanks.py @@ -0,0 +1,15 @@ +import logger + + +class Thanks: + def process(self, bot, channel, sender, message): + """ + Le message est pris en compte si l'utilisateur remercie le bot. + """ + if message.lower() == "!merci" or ( + bot.nickname.lower() in message.lower() and "merci" in message + ): + logger.info("!merci caught.") + bot.send(channel, f"{sender}, de rien \\o/") + + return True diff --git a/bot/hooks/version.py b/bot/hooks/version.py new file mode 100644 index 0000000..5a4e194 --- /dev/null +++ b/bot/hooks/version.py @@ -0,0 +1,13 @@ +import logger + + +class Version: + def process(self, bot, channel, sender, message): + """ + Si la commande est bonne, le bot renvoie sa version. + """ + if message.lower() == "!version": + logger.info("!version caught.") + bot.send(channel, bot.settings.VERSION) + + return True diff --git a/bot/review/__init__.py b/bot/review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/review/aliases.py b/bot/review/aliases.py new file mode 100644 index 0000000..0fc1dc8 --- /dev/null +++ b/bot/review/aliases.py @@ -0,0 +1,31 @@ +from datetime import datetime + + +class Aliases: + def __init__(self, filepath): + self.filepath = filepath + self.users = {} + self.load() + + def __getitem__(self, key): + if key in self.users: + return self.users[key] + else: + try: + index = [key.lower() for key in self.users].index(key.lower()) + return self.users[list(self.users.keys())[index]] + except ValueError: + # key really not found + pass + return key + + def load(self): + with open(self.filepath) as file_handle: + lines = file_handle.read().splitlines() + + for line in lines: + if line.strip() != "" and not line.startswith("#"): + datas = line.split("=") + real_name = datas[0] + for nick in datas[1].split(","): + self.users[nick] = real_name diff --git a/bot/review/review.py b/bot/review/review.py new file mode 100644 index 0000000..ad28873 --- /dev/null +++ b/bot/review/review.py @@ -0,0 +1,250 @@ +from datetime import datetime +import locale +from textwrap import fill + +from bot.review.topic import Message, Topic +from bot.review.aliases import Aliases + + +LENGTH_LINE = 80 + + +class Review: + def __init__(self): + self.participants = [] + self.topics = [] + self.messages = [] + self.owner = None + self.started = False + self.ended = False + self.start_time = None + self.end_time = None + + @property + def is_started(self): + return self.started + + @property + def is_ended(self): + return self.ended + + @property + def last_topic(self): + if len(self.topics) >= 2: + return self.topics[-2] + + @property + def current_topic(self): + if len(self.topics) > 0: + return self.topics[-1] + + @property + def has_participants(self): + return len(self.participants) > 0 + + @property + def individual_topics(self): + return [topic for topic in self.topics if topic.individual] + + @property + def collective_topics(self): + return [topic for topic in self.topics if topic.collective] + + @property + def has_individual_topic(self): + return len(self.individual_topics) > 0 + + @property + def has_collective_topic(self): + return len(self.collective_topics) > 0 + + @property + def user_count(self): + return len(self.participants) + + @property + def duration(self): + return (self.end_time - self.start_time).seconds // 60 if self.ended else None + + @property + def year(self): + return self.start_time.year + + def is_owner(self, owner): + return owner.lower() == self.owner.lower() + + def new_topic(self, title, collective=True): + self.topics.append(Topic(title, collective)) + + def new_collective_topic(self, title): + self.new_topic(title) + + def new_individual_topic(self, title): + self.new_topic(title, False) + + def add_message(self, sender, message): + self.messages.append(Message(sender, message)) + + def add_input(self, sender, message): + if self.current_topic: + if sender not in self.participants: + self.participants.append(sender) + self.current_topic.add_message(sender, message) + + def start(self, owner): + self.started = True + self.owner = owner + self.start_time = datetime.today() + + def close(self): + self.ended = True + self.end_time = datetime.today() + + def cancel(self): + self.participants = [] + self.topics = [] + self.messages = [] + self.owner = None + self.started = False + self.ended = False + self.start_time = None + self.end_time = None + + def report(self, stats, user_aliases_filepath): + locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + formatter = "%A %d %B %Y" + hour_formatter = "%Hh%M" + + aliases = Aliases(user_aliases_filepath) + + def add_center(text, character=" "): + text_size = len(text) + 2 + left_size = (LENGTH_LINE - text_size) // 2 + right_size = LENGTH_LINE - left_size - text_size + result = character * left_size + " " + text + if character != " ": + result += " " + character * right_size + result += "\n" + return result + + content = "=" * LENGTH_LINE + "\n" + content += add_center("Revue de la semaine en cours") + content += "\n" + content += add_center(self.start_time.strftime(formatter)) + content += "=" * LENGTH_LINE + "\n" + content += "\n" + content += "\n" + content += "=" * LENGTH_LINE + "\n" + content += "\n" + content += add_center("Personnes participantes", "-") + for participant in self.participants: + content += f"* {aliases[participant]} ({participant})\n" + + if self.has_individual_topic: + for participant in self.participants: + content += "\n" + content += "=" * LENGTH_LINE + "\n" + content += "\n" + content += add_center(f"{aliases[participant]} ({participant})", "-") + for topic in self.individual_topics: + if topic.has_participant(participant): + content += "\n" + content += f"=== {topic.title} ===\n" + content += "\n" + for message in topic.get_messages(participant): + content += fill(f"* {message.text}", width=LENGTH_LINE) + content += "\n" + + if self.has_collective_topic: + for topic in self.collective_topics: + content += "\n" + content += "=" * LENGTH_LINE + "\n" + content += add_center(topic.title) + content += "=" * LENGTH_LINE + "\n" + content += "\n" + for message in topic.get_messages(): + content += fill( + f"* {message.author} : {message.text}", + width=LENGTH_LINE, + ) + content += "\n" + + content += "\n" + content += add_center("Log IRC brut") + content += "\n" + for message in self.messages: + content += fill(f"* {message.author} : {message.text}\n", width=LENGTH_LINE) + content += "\n" + + # Add statistics + content += "\n" + content += add_center("Statistiques") + content += "\n" + content += ( + f"C'était la {stats.size + 1}e revue hebdomadaire de l'April, " + f"la {stats.year_review(self.year) + 1}e de l'année {self.year}.\n" + ) + content += ( + "Horaire de début de la revue : " + f"{self.start_time.strftime(hour_formatter)}\n" + ) + content += ( + "Horaire de fin de la revue : " + f"{self.end_time.strftime(hour_formatter)}\n" + ) + content += f"Durée de la revue : {self.duration} minutes\n" + content += f"Nombre de personnes participantes : {self.user_count}\n" + if self.user_count < stats.max_users: + content += ( + "La participation moyenne aux revues est " + f"de {stats.avg_users:.1f} personnes.\n" + ) + elif self.user_count == stats.max_users: + content += ( + "\\o/ Record de participation égalé \\o/ Le précédent record " + f"de {stats.max_users} personnes était le " + f"{stats.biggest.date.strftime(formatter)}.\n" + ) + else: + content += ( + f"*\\o/* Nouveau record de participation : {self.user_count} " + "personnes ! *\\o/* Le précédent record était de " + f"{stats.max_users} personnes le " + f"{stats.biggest.date.strftime(formatter)}.\n" + ) + + percentage = ( + stats.users_board.datas[self.user_count] / stats.users_board.sum * 100 + ) + content += fill( + "Statistiques sur la participation à la revue " + f"({self.user_count} personnes) : position " + f"{stats.users_board.position(self.user_count)} " + f"(min.={stats.users_board.min}, " + f"moy.={stats.users_board.avg:.1f}, " + f"max.={stats.users_board.max}) " + f"fréquence {stats.users_board[self.user_count]}" + f"/{stats.users_board.sum} " + f"({percentage:.0f} %) ", + width=LENGTH_LINE, + ) + content += "\n" + + percentage = ( + stats.users_board.datas[self.user_count] / stats.users_board.sum * 100 + ) + content += fill( + "Statistiques sur la durée de la revue " + f"({self.duration} min) : position " + f"{stats.durations_board.position(self.duration)} " + f"(min.={stats.durations_board.min} min, " + f"moy.={stats.durations_board.avg:.1f} min, " + f"max.={stats.durations_board.max} min) " + f"fréquence {stats.durations_board[self.duration]}" + f"/{stats.durations_board.sum} " + f"({percentage:.0f} %) ", + width=LENGTH_LINE, + ) + content += "\n" + + return content diff --git a/bot/review/stats.py b/bot/review/stats.py new file mode 100644 index 0000000..e64ee57 --- /dev/null +++ b/bot/review/stats.py @@ -0,0 +1,177 @@ +from dataclasses import dataclass +from datetime import datetime +import re + + +@dataclass +class ReviewData: + date: datetime + user_count: int + duration: int + + +class Board: + def __init__(self, datas): + self.datas = {} + self.init(datas) + + def init(self, datas): + pass + + def __repr__(self): + return " ".join([f"{key} ({value})" for key, value in self.datas.items()]) + + def __getitem__(self, key): + if key in self.datas: + return self.datas[key] + return 0 + + def position(self, key): + if key in self.datas: + return sorted( + self.datas, key=lambda x: self.datas[x] + ).index(key) + 1 + + @property + def min(self): + if self.datas == []: + return 0 + return min(self.datas.values()) + + @property + def max(self): + if self.datas == []: + return 0 + return max(self.datas.values()) + + @property + def avg(self): + if self.datas == []: + return 0 + return self.sum / len(self.datas) + + @property + def sum(self): + if self.datas == []: + return 0 + return sum(self.datas.values()) + + +class UserBoard(Board): + def init(self, datas): + for data in datas: + if data.user_count not in self.datas: + self.datas[data.user_count] = 0 + self.datas[data.user_count] += 1 + + +class DurationBoard(Board): + def init(self, datas): + for data in datas: + if data.duration not in self.datas: + self.datas[data.duration] = 0 + self.datas[data.duration] += 1 + + +class ReviewStats: + def __init__(self, filepath): + self.filepath = filepath + self.datas = [] + self.users_board = None + self.durations_board = None + + @property + def size(self): + return len(self.datas) + + @property + def first(self): + return sorted(self.datas, key=lambda data: data.date)[0] if self.datas else None + + @property + def biggest(self): + return ( + sorted(self.datas, key=lambda data: data.user_count)[-1] + if self.datas + else None + ) + + @property + def min_users(self): + if self.datas == []: + return 0 + return min(self.datas, key=lambda data: data.user_count).user_count + + @property + def max_users(self): + if self.datas == []: + return 0 + return max(self.datas, key=lambda data: data.user_count).user_count + + @property + def avg_users(self): + if self.datas == []: + return 0 + return sum([data.user_count for data in self.datas]) / self.size + + @property + def min_duration(self): + if self.datas == []: + return 0 + datas = [data for data in self.datas if data.duration is not None] + return min(datas, key=lambda data: data.duration).duration + + @property + def max_duration(self): + if self.datas == []: + return 0 + datas = [data for data in self.datas if data.duration is not None] + return max(datas, key=lambda data: data.duration).duration + + @property + def avg_duration(self): + if self.datas == []: + return 0 + datas = [data for data in self.datas if data.duration is not None] + return sum([data.duration for data in datas]) / len(datas) + + def year_review(self, year): + return len([data for data in self.datas if data.date.year == year]) + + def load(self): + self.datas = [] + try: + with open(self.filepath) as file_handle: + lines = file_handle.read().splitlines() + for line in lines: + if line.strip() != "": + datas = re.split(r"\s+", line) + if len(datas) == 2: + self.datas.append( + ReviewData( + datetime.strptime(datas[0], "%Y%m%d-%Hh%M"), + int(datas[1]), + None, + ) + ) + else: + self.datas.append( + ReviewData( + datetime.strptime(datas[0], "%Y%m%d-%Hh%M"), + int(datas[1]), + int(datas[2]), + ) + ) + except FileNotFoundError: + # no file, no stats + pass + self.users_board = UserBoard(self.datas) + self.durations_board = DurationBoard(self.datas) + + def save(self): + with open(self.filepath, "wt") as file_handle: + for data in self.datas: + file_handle.write( + f"{data.date.strftime('%Y%m%d-%Hh%M')}\t" + f"{data.user_count}\t{data.duration}\n" + ) diff --git a/bot/review/topic.py b/bot/review/topic.py new file mode 100644 index 0000000..6b889c3 --- /dev/null +++ b/bot/review/topic.py @@ -0,0 +1,60 @@ +from datetime import datetime + + +class Message: + def __init__(self, author, text): + self.author = author + self.text = text + self.timestamp = datetime.today() + + +class Topic: + def __init__(self, title, collective=True): + self.title = title + self.collective = collective + self.participants = [] + self.messages = [] + self.start_time = datetime.today() + self.end_time = None + + @property + def duration(self): + if self.end_time: + minutes = (self.end_time - self.start_time).seconds // 60 + seconds = (self.end_time - self.start_time).seconds % 60 + else: + minutes = (datetime.today() - self.start_time).seconds // 60 + seconds = (datetime.today() - self.start_time).seconds % 60 + return f"{minutes}:{seconds:02d}" + + @property + def individual(self): + return not self.collective + + def add_message(self, sender, message): + self.messages.append(Message(sender, message)) + if sender not in self.participants: + self.participants.append(sender) + + def cancel_previous(self, author): + messages = [message for message in self.messages if message.author == author] + if len(messages) == 0: + return + message = messages[-1].text + self.messages.remove(messages[-1]) + + return message + + def get_messages(self, author=None): + if self.collective: + return self.messages + return [message for message in self.messages if message.author == author] + + def has_participant(self, author): + return author in self.get_participants() + + def get_participants(self): + return set([message.author for message in self.messages]) + + def close(self): + self.end_time = datetime.today() diff --git a/hebdobot.py b/hebdobot.py new file mode 100644 index 0000000..19daeb1 --- /dev/null +++ b/hebdobot.py @@ -0,0 +1,24 @@ +import click + +from ircbot import IrcBot +import settings +import logger + + +@click.command() +@click.option("--version", is_flag=True, help="Display bot version and exit.") +def main(version): + if version: + click.echo(f"Hebdobot {settings.VERSION}") + exit(0) + + logger.info( + "--==============================INIT====" + "======================================--" + ) + logger.info(f"Hebdobot {settings.VERSION}") + IrcBot(settings).start() + + +if __name__ == "__main__": + main() diff --git a/ircbot.py b/ircbot.py new file mode 100644 index 0000000..eeaef58 --- /dev/null +++ b/ircbot.py @@ -0,0 +1,54 @@ +import time + +import irc.bot +import irc.strings + +from bot.hebdobot import Hebdobot + + +class IrcBot(irc.bot.SingleServerIRCBot): + def __init__(self, settings): + # Récupération des paramètres + self.settings = settings + self.server = self.settings.IRC_SERVER + self.port = self.settings.IRC_PORT + self.channel = self.settings.IRC_CHANNEL + self.nickname = self.settings.IRC_NICK + self.password = self.settings.IRC_PASSWORD + + self.bot = Hebdobot(self.settings, self.channel, self.nickname) + + # Démarrage et connexion à IRC + irc.bot.SingleServerIRCBot.__init__( + self, [(self.server, self.port)], self.nickname, self.nickname + ) + + def on_nicknameinuse(self, connection, event): + # TODO: Re-tester si son propre nom est disponible plus tard. + self.nickname = self.nickname + "_" + self.bot.nickname = self.nickname + connection.nick(self.nickname) + + def on_welcome(self, connection, event): + connection.join(self.channel) + + def on_privmsg(self, connection, event): + channel = event.target + sender = event.source.nick + message = event.arguments[0] + answers = self.bot.on_private_message(channel, sender, message) + for answer in answers: + self.connection.privmsg(answer.target, answer.message) + + def on_pubmsg(self, connection, event): + channel = event.target + sender = event.source.nick + message = event.arguments[0] + answers = self.bot.on_public_message(channel, sender, message) + for answer in answers: + self.connection.privmsg(answer.target, answer.message) + + def send_multiple(self, target, messages): + for message in messages: + self.connection.privmsg(target, message) + time.sleep(self.settings.IRC_DELAY) diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..b371da6 --- /dev/null +++ b/logger.py @@ -0,0 +1,29 @@ +import logging + +import settings + + +def init_logger(): + logging.basicConfig( + filename=settings.LOGFILE, + encoding="utf-8", + level=settings.LOGLEVEL, + format=settings.LOGFORMAT, + datefmt=settings.LOGDATE, + ) + return logging.getLogger(__name__) + + +def info(message): + logger = init_logger() + logger.info(f"INFO - {message}") + + +def error(message): + logger = init_logger() + logger.error(f"ERROR - {message}") + + +def debug(message): + logger = init_logger() + logger.debug(f"DEBUG - {message}") diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b5a4eb8 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +black +pytest +pytest-cov diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..15e3a7a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +click==8.1.7 +irc==20.3.0 diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..6129f76 --- /dev/null +++ b/settings.py @@ -0,0 +1,34 @@ +import logging +import os + + +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + +VERSION = "3.0.0" + +# Log File +LOGFILE = os.environ.get("LOGFILE", os.path.join(BASE_DIR, "hebdobot.log")) +LOGLEVEL = os.environ.get("LOGLEVEL", logging.DEBUG) +LOGFORMAT = os.environ.get("LOGFORMAT", "%(asctime)s.%(msecs)03d - %(message)s") +LOGDATE = os.environ.get("LOGDATE", "%Y-%m-%d %H:%M:%S") + +# IRC configuration +IRC_SERVER = os.environ.get("IRC_SERVER", "irc.libera.chat") +IRC_PORT = int(os.environ.get("IRC_PORT", 6667)) +IRC_CHANNEL = os.environ.get("IRC_CHANNEL", " #bidibot") +IRC_NICK = os.environ.get("IRC_NICK", " Testbot") +IRC_PASSWORD = os.environ.get("IRC_PASSWORD", " ") +IRC_DELAY = float(os.environ.get("IRC_DELAY", 0.5)) # Délai entre plusieurs messages + +# User Alias +USER_ALIASES = "" + +# Review +REVIEW_DIRECTORY = os.environ.get("REVIEW_DIRECTORY", os.path.join(BASE_DIR, "reviews")) +REVIEW_PASTEBIN = "" +REVIEW_STATS = os.environ.get("REVIEW_STATS", os.path.join(REVIEW_DIRECTORY, "reviewstats.csv")) + +# Mastodon +MASTODON_SERVER = "" +MASTODON_NAME = "" +MASTODON_PASSWORD = "" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/datas/20231201-log-irc-revue-hebdomadaire.txt b/tests/datas/20231201-log-irc-revue-hebdomadaire.txt new file mode 100644 index 0000000..c80f07d --- /dev/null +++ b/tests/datas/20231201-log-irc-revue-hebdomadaire.txt @@ -0,0 +1,445 @@ +================================================================================ + Revue de la semaine en cours + + vendredi 01 décembre 2023 +================================================================================ + + +================================================================================ + +--------------------------- Personnes participantes ---------------------------- +* Ccccc Ccccc (ccccc) +* Lllll Lllll (lllll) +* Mmmmm Mmmmm (mmmmm) +* Ooooo Ooooo (ooooo) +* Bbbbb Bbbbb (bbbbb) + +================================================================================ + +----------------------------- Ccccc Ccccc (ccccc) ------------------------------ + +=== 1/ Actions passées ou en cours === + +* vérification nouvels albums LAV et CdL et retours à l'équipe photos +* suivi inscriptions pour tenue du stand à OSXP et échanges divers avec les +bénévoles +* passage en revue tâches admins en vue de la réu mensuelle +* lecture PF/PV Capitole du Libre et réactions diverses +* préparation intervention dans LAV +* participation à LAV +* relances pour possible sujet principal LAV +* participation à échange sur possible nouveau goody +* prépa conf diversité et inclusivité pour OSXP +* point conf OSXP avec mmmmm +* point hebdo avec mmmmm +* divers paie novembre +* off mercredi 29 novembre + +=== 2/ Actions à venir === + +* création badges pour bénévoles OSXP +* suite préparation conf OSXP et peut-être et possible répétition avec public +* présence sur salon OSXP : stand, conférence, tour des orgas avec bbbbb +* fixer dates du prochain LEF +* traitement courriels en souffrance +* divers goodies + +================================================================================ + +----------------------------- Lllll Lllll (lllll) ------------------------------ + +=== 1/ Actions passées ou en cours === + +* dépilage courriels et veille suite à arrêt maladie +* relecture pad préparation conf "diversité" +* Lecture du communiqué sur l'état des lieux des logiciels de la recherche +* mise à jour fichier texte de suivi des demandes CADA +* LAV : relecture chronique Marie-Odile, tâches post-émission du 7/11 en retard +* LAV : régie émission du 28/11 +* lecture dossier candidatures label TNL 2023 (en cours) +* préparation table ronde collectivité à OSXP +* suivi dossier en cours +* Tâches récurrentes + +=== 2/ Actions à venir === + +* participation réunion jury TNL le 4/12, finir lecture dossier candidature +* présence OSXP 6 et 7 décembre (au stand, conf, remise label TNL), animation +table ronde collectivité +* LAV : divers pour émission à venir (remplir la grille de la rentrée) +* suivi dossier +* prise de contact tel avec membre wikilibriste +* Tâches récurrentes et divers selon l'actu + +================================================================================ + +----------------------------- Mmmmm Mmmmm (mmmmm) ------------------------------ + +=== 1/ Actions passées ou en cours === + +* Lecture du communiqué sur l'état des lieux des logiciels de la recherche +* Admin sys : Préparation réunion admin sys du 29 novembre 2023. Présence +réunion +* Admin sys : Divers pour utilisation excessive du CPU par Firefox +* Chapril : Signalement problème Sympa suite à la migration +* Équipe : point avec ccccc +* Lettres d'information : Rédaction et diffusion lettre d'information interne du +1er décembre +* Lettres d'information : Rédaction lettre d'information publique du 1er +décembre +* Évènements : Point avec ccccc pour préparation conf diversité/inclusivité à +OSXP +* Libre à vous ! Préparation et animation émission du 28 novembre 2023, actions +post émission +* Libre à vous ! Actions post émission du 21 novembre 2023 +* Libre à vous ! Divers pour AntennaPod et le flux RSS +* Libre à vous ! Double des clés du studio pour l'équipe régie +* Divers pour résultats Prix de science ouverte du logiciel libre de recherche +* Récupération de l'Expolibre utilisée par la Cie Terraquée à Paris 8 +* Divers pour AG (statuts, conférence d'ouverture) +* Divers pour avenir April, suivi possibles bénévoles, diversité et inclusivité +etc +* Validation de la revue de presse +* Tâches récurrentes + +=== 2/ Actions à venir === + +* Présence soirée Cause Commune vendredi 1er décembre à partir de 19 h 30 +* Lettres d'information : Finalisation et diffusion lettre d'information +publique du 1er décembre +* Évènements : Suite préparation conf diversité/inclusivité à OSXP +* Libre à vous ! Investigation problème AntennaPod et flux RSS +* Évènements : Présence salon OSXP +* Le reste en fonction du temps disponible +* Possible absence cet aprem + +================================================================================ + +----------------------------- Ooooo Ooooo (ooooo) ------------------------------ + +=== 1/ Actions passées ou en cours === + +* Transcriptions +* Participation à l'émission de LAV de mardi 28/11 « Au coeur de l'April » +* Envoir prochaine chronique à Laure-Élise pour enregistrement + +================================================================================ + +----------------------------- Bbbbb Bbbbb (bbbbb) ------------------------------ + +=== 1/ Actions passées ou en cours === + +* radio au coeur de l'April +* lecture mails +* preparation salon osxp +* recherche prochaine étape tour des gull +* grosse procastinisation sur écriture de textes +* réflexion sur nouveau goody + +=== 2/ Actions à venir === + +* écriture tour des gull +* promotion Ada et mr zingemann +* présence salon osxp +* modération du pouet +* animation (ou pas) track politique / village du libre / stand +* tour des entreprises adhérentes de l'April avec ccccc + +================================================================================ + 3/ Points de blocage / points en retard corrigés cette semaine +================================================================================ + +* mmmmm : Firefox sur mon laptop utilise trop de ressources (CPU notamment), +problème peut-être réglé mais pas sûr cf https://agir.april.org/issues/6276 + +================================================================================ + 4/ Points de blocage existants / points en retard à traiter +================================================================================ + +* lllll : prise de contact tel avec membre wikilibriste +* lllll : attaquer lecture "Géopolitique du numérique" +* ccccc : impression nouvel autocollant + +================================================================================ + 5/ Points forts de la semaine +================================================================================ + +* ccccc : moins de pluie +* ooooo : Une belle émission mardi 28/11 +* lllll : plaisir à refaire une régie solo (ça faisait longtemps) +* ccccc : un sujet animé par une nouvelle bénévole + +================================================================================ + 6/ Points de vigilance de la semaine +================================================================================ + +* ooooo : Souci avec les statistiques du site Libre à lire ! > 37 000 visites le +30 novembre ! +* lllll : convalescence un peu longue, mais ça va bien mieux. Hâte de repasser +au local + +================================================================================ + 7/ Points forts de la réunion +================================================================================ + +* ccccc : plus de monde que la dernière fois + +================================================================================ + 8/ Points de vigilance de la réunion +================================================================================ + +* lllll : Les clientes qui dérangent la présidente en pleine réunion +* ccccc : baisse de 5 dégérs entre la petite salle de réu et le bureau +* mmmmm : Penser à faire les annonces twitter et mastodon +* ccccc : baisse de 5 degrés entre la petite salle de réu et le bureau + + Log IRC brut + +* Hebdobot : % Début de la réunion hebdomadaire +* Hebdobot : % rappel : toute ligne commençant par % sera considérée comme un +commentaire et non prise en compte dans la synthèse +* Hebdobot : % pour connaître le point courant, taper !courant +* lllll : # 1/ Actions passées ou en cours +* Hebdobot : Sujet individuel : 1/ Actions passées ou en cours +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* ccccc : vérification nouvels albums LAV et CdL et retours à l'équipe photos +* lllll : dépilage courriels et veille suite à arrêt maladie +* mmmmm : Lecture du communiqué sur l'état des lieux des logiciels de la +recherche +* ooooo : Transcriptions +* bbbbb : radio au coeur de l'April +* mmmmm : Admin sys : Préparation réunion admin sys du 29 novembre 2023. +Présence réunion +* lllll : relecture pad préparation conf "diversité" +* ccccc : suivi inscriptions pour tenue du stand à OSXP et échanges divers avec +les bénévoles +* mmmmm : Admin sys : Divers pour utilisation excessive du CPU par Firefox +* bbbbb : lecture mails +* ooooo : Participation à l'émission de LAV de mardi 28/11 « Au coeur de l'April +» +* lllll : relecture pad préparation conf "diversité" +* lllll : !oops +* Hebdobot : lllll, suppression de votre précédente entrée : relecture pad +préparation conf "diversité" +* bbbbb : preparation salon osxp +* mmmmm : Admin sys : Signalement problème Sympa suite à la migration +* ccccc : passage en revue tâches admins en vue de la réu mensuelle +* mmmmm : !oops +* Hebdobot : mmmmm, suppression de votre précédente entrée : Admin sys : +Signalement problème Sympa suite à la migration +* lllll : Lecture du communiqué sur l'état des lieux des logiciels de la +recherche +* mmmmm : Chapril : Signalement problème Sympa suite à la migration +* lllll : mise à jour fichier texte de suivi des demandes CADA +* ccccc : lecture PF/PV Capitole du Libre et réactions diverses +* mmmmm : Équipe : point avec ccccc +* lllll : LAV : relecture chronique Marie-Odile, tâches post-émission du 7/11 en +retard +* mmmmm : Lettres d'information : Rédaction et diffusion lettre d'information +interne du 1er décembre +* ccccc : préparation intervention dans LAV +* ooooo : Envoir prochaine chronique à Laure-Élise pour enregistrement +* lllll : LAV : régie émission du 28/11 +* ccccc : participation à LAV +* mmmmm : Lettres d'information : Rédaction lettre d'information publique du 1er +décembre +* lllll : lecture dossier candidatures label TNL 2023 (en cours) +* ooooo : % fini +* bbbbb : recherche prochaine étape tour des gull +* lllll : préparation table ronde collectivité à OSXP +* ccccc : relances pour possible sujet principal LAV +* lllll : suivi dossier en cours +* mmmmm : Évènements : Point avec ccccc pour préparation conf +diversité/inclusivité à OSXP +* bbbbb : grosse procastinisation sur écriture de textes +* lllll : Tâches récurrentes +* ccccc : participation à échange sur possible nouveau goody +* mmmmm : Libre à vous ! Préparation et animation émission du 28 novembre 2023, +actions post émission +* mmmmm : Libre à vous ! Actions post émission du 21 novembre 2023 +* mmmmm : Libre à vous ! Divers pour AntennaPod et le flux RSS +* eipoca : %je ne participe pas à la RH, j'ai besoin de ce temps pour +retranscrire mes échanges ac la presta +* mmmmm : Libre à vous ! Double des clés du studio pour l'équipe régie +* ccccc : prépa conf diversité et inclusivité pour OSXP +* bbbbb : réflexion sur nouveau goody +* mmmmm : Divers pour résultats Prix de science ouverte du logiciel libre de +recherche +* ccccc : point conf OSXP avec mmmmm +* mmmmm : Récupération de l'Expolibre utilisée par la Cie Terraquée à Paris 8 +* ccccc : point hebdo avec mmmmm +* mmmmm : Divers pour AG (statuts, conférence d'ouverture) +* ccccc : divers paie novembre +* mmmmm : Divers pour avenir April, suivi possibles bénévoles, diversité et +inclusivité etc +* mmmmm : Validation de la revue de presse +* mmmmm : Tâches récurrentes +* mmmmm : %fini +* ccccc : off mercredi 29 novembre +* ccccc : %fini +* lllll : % bbbbb fini ? +* bbbbb : %fini +* lllll : % on va passer à la suite : Actions à venir +* lllll : # 2/ Actions à venir +* Hebdobot : % durée du point 1/ Actions passées ou en cours : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 2/ +Actions à venir +* Hebdobot : Sujet individuel : 2/ Actions à venir +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* lllll : participation réunion jury TNL le 4/12, finir lecture dossier +candidature +* mmmmm : Présence soirée Cause Commune vendredi 1er décembre à partir de 19 h +30 +* ccccc : création badges pour bénévoles OSXP +* bbbbb : écriture tour des gull +* ooooo : % ras +* lllll : présence OSXP 6 et 7 décembre (au stand, conf, remise label TNL), +animation table ronde collectivité +* bbbbb : promotion Ada et mr zingemann +* mmmmm : Lettres d'information : Finalisation et diffusion lettre d'information +publique du 1er décembre +* lllll : LAV : divers pour émission à venir (remplir la grille de la rentrée) +* bbbbb : présence salon osxp +* bbbbb : modération du pouet +* ccccc : suite préparation conf OSXP et peut-être et possible répétition avec +public +* lllll : suivi dossier +* mmmmm : % bbbbb tu as reçu mon courriel au sujet du livre ? (demande de +citation) +* lllll : prise de contact tel avec membre wikilibriste +* mmmmm : Évènements : Suite préparation conf diversité/inclusivité à OSXP +* ccccc : présence sur salon OSXP : stand, conférence, tour des orgas avec bbbbb +* bbbbb : animation (ou pas) track politique / village du libre / stand +* lllll : Tâches récurrentes et divers selon l'actu +* lllll : %fini +* bbbbb : tour des entreprises adhérentes de l'April +* ccccc : fixer dates du prochain LEF +* bbbbb : !oups +* Hebdobot : bbbbb, suppression de votre précédente entrée : tour des +entreprises adhérentes de l'April +* mmmmm : Libre à vous ! Investigation problème AntennaPod et flux RSS +* ccccc : traitement courriels en souffrance +* bbbbb : tour des entreprises adhérentes de l'April avec ccccc +* ccccc : divers goodies +* mmmmm : Évènements : Présence salon OSXP +* ccccc : %fini +* bbbbb : %fini +* lllll : % mmmmm fini ? +* bbbbb : %cliente.. désolée +* mmmmm : Le reste en fonction du temps disponible +* mmmmm : Possible absence cet aprem +* mmmmm : %fini +* lllll : %on va passer à la suite : Points de blocage / points en retard +* lllll : ## 3/ Points de blocage / points en retard corrigés cette semaine +* Hebdobot : % durée du point 2/ Actions à venir : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 3/ +Points de blocage / points en retard corrigés cette semaine +* Hebdobot : Sujet collectif : 3/ Points de blocage / points en retard corrigés +cette semaine +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* ccccc : %ras +* lllll : %ras +* ooooo : % ras +* mmmmm : Firefox sur mon laptop utilise trop de ressources (CPU notamment), +problème peut-être réglé mais pas sûr cf https://agir.april.org/issues/6276 +* mmmmm : %fini +* lllll : %on va passer à la suite : Points de blocage / points en retard +existants à traiter +* lllll : ## 4/ Points de blocage existants / points en retard à traiter +* Hebdobot : % durée du point 3/ Points de blocage / points en retard corrigés +cette semaine : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 4/ +Points de blocage existants / points en retard à traiter +* Hebdobot : Sujet collectif : 4/ Points de blocage existants / points en retard +à traiter +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* lllll : prise de contact tel avec membre wikilibriste +* lllll : attaquer lecture "Géopolitique du numérique" +* ccccc : impression nouvel autocollant +* mmmmm : %ras +* ccccc : %fini +* ooooo : % ras +* lllll : % on va passer à la suite : Points Forts et Points de Vigilance de la +semaine +* lllll : ## 5/ Points forts de la semaine +* Hebdobot : % durée du point 4/ Points de blocage existants / points en retard +à traiter : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 5/ +Points forts de la semaine +* Hebdobot : Sujet collectif : 5/ Points forts de la semaine +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* mmmmm : %ras +* ccccc : moins de pluie +* ccccc : %fini +* ooooo : Une belle émission mardi 28/11 +* lllll : plaisir à refaire une régie solo (ça faisait longtemps) +* ccccc : un sujet animé par une nouvelle bénévole +* lllll : % on va passer à la suite : Points de Vigilance de la semaine +* ccccc : %fini +* lllll : ## 6/ Points de vigilance de la semaine +* Hebdobot : % durée du point 5/ Points forts de la semaine : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 6/ +Points de vigilance de la semaine +* Hebdobot : Sujet collectif : 6/ Points de vigilance de la semaine +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* ooooo : Souci avec les statistiques du site Libre à lire ! > 37 000 visites le +30 novembre ! +* mmmmm : %ras +* lllll : convalescence un peu longue, mais ça va bien mieux. Hâte de repasser +au local +* ccccc : %ras +* ooooo : % fini +* lllll : % on va passer à la suite : Points Forts et Points de Vigilance de la +réunion +* lllll : ## 7/ Points forts de la réunion +* Hebdobot : % durée du point 6/ Points de vigilance de la semaine : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 7/ +Points forts de la réunion +* Hebdobot : Sujet collectif : 7/ Points forts de la réunion +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* ooooo : %ras +* mmmmm : %ras +* lllll : % un peu long à lancer dis donc +* lllll : %ra +* lllll : %ras +* ccccc : plus de monde que la dernière fois +* ccccc : %fini +* lllll : % on va passer à la suite : Points de Vigilance de la réunion +* lllll : ## 8/ Points de vigilance de la réunion +* Hebdobot : % durée du point 7/ Points forts de la réunion : 0:00 +* Hebdobot : % ccccc lllll mmmmm ooooo bbbbb, on va passer à la suite : 8/ +Points de vigilance de la réunion +* Hebdobot : Sujet collectif : 8/ Points de vigilance de la réunion +* Hebdobot : % 1 minute max +* Hebdobot : % si rien à signaler vous pouvez écrire % ras +* Hebdobot : % quand vous avez fini vous le dites par % fini +* lllll : Les clientes qui dérangent la présidente en pleine réunion +* mmmmm : % lllll tu as fait les annonces twitter et mastodon ? +* ccccc : baisse de 5 dégérs entre la petite salle de réu et le bureau +* ccccc : %fini +* lllll : % ah non, j'y penserai dorénavant +* mmmmm : Penser à faire les annonces twitter et mastodon +* mmmmm : %fini +* ccccc : %oups +* ccccc : baisse de 5 degrés entre la petite salle de réu et le bureau +* ccccc : %fini +* lllll : % Fin de la revue hebdomadaire, merci à vous +* lllll : !fin +* Hebdobot : % durée du point 8/ Points de vigilance de la réunion : 0:00 + + Statistiques + +C'était la 314e revue hebdomadaire de l'April, la 1e de l'année 2023. +Horaire de début de la revue : 12h00 diff --git a/tests/datas/irc.txt b/tests/datas/irc.txt new file mode 100644 index 0000000..2bc1aba --- /dev/null +++ b/tests/datas/irc.txt @@ -0,0 +1,152 @@ +lllll : # 1/ Actions passées ou en cours +ccccc : vérification nouvels albums LAV et CdL et retours à l'équipe photos +lllll : dépilage courriels et veille suite à arrêt maladie +mmmmm : Lecture du communiqué sur l'état des lieux des logiciels de la recherche +ooooo : Transcriptions +bbbbb : radio au coeur de l'April +mmmmm : Admin sys : Préparation réunion admin sys du 29 novembre 2023. Présence réunion +lllll : relecture pad préparation conf "diversité" +ccccc : suivi inscriptions pour tenue du stand à OSXP et échanges divers avec les bénévoles +mmmmm : Admin sys : Divers pour utilisation excessive du CPU par Firefox +bbbbb : lecture mails +ooooo : Participation à l'émission de LAV de mardi 28/11 « Au coeur de l'April » +lllll : relecture pad préparation conf "diversité" +lllll : !oops +bbbbb : preparation salon osxp +mmmmm : Admin sys : Signalement problème Sympa suite à la migration +ccccc : passage en revue tâches admins en vue de la réu mensuelle +mmmmm : !oops +lllll : Lecture du communiqué sur l'état des lieux des logiciels de la recherche +mmmmm : Chapril : Signalement problème Sympa suite à la migration +lllll : mise à jour fichier texte de suivi des demandes CADA +ccccc : lecture PF/PV Capitole du Libre et réactions diverses +mmmmm : Équipe : point avec ccccc +lllll : LAV : relecture chronique Marie-Odile, tâches post-émission du 7/11 en retard +mmmmm : Lettres d'information : Rédaction et diffusion lettre d'information interne du 1er décembre +ccccc : préparation intervention dans LAV +ooooo : Envoir prochaine chronique à Laure-Élise pour enregistrement +lllll : LAV : régie émission du 28/11 +ccccc : participation à LAV +mmmmm : Lettres d'information : Rédaction lettre d'information publique du 1er décembre +lllll : lecture dossier candidatures label TNL 2023 (en cours) +ooooo : % fini +bbbbb : recherche prochaine étape tour des gull +lllll : préparation table ronde collectivité à OSXP +ccccc : relances pour possible sujet principal LAV +lllll : suivi dossier en cours +mmmmm : Évènements : Point avec ccccc pour préparation conf diversité/inclusivité à OSXP +bbbbb : grosse procastinisation sur écriture de textes +lllll : Tâches récurrentes +ccccc : participation à échange sur possible nouveau goody +mmmmm : Libre à vous ! Préparation et animation émission du 28 novembre 2023, actions post émission +mmmmm : Libre à vous ! Actions post émission du 21 novembre 2023 +mmmmm : Libre à vous ! Divers pour AntennaPod et le flux RSS +eipoca : %je ne participe pas à la RH, j'ai besoin de ce temps pour retranscrire mes échanges ac la presta +mmmmm : Libre à vous ! Double des clés du studio pour l'équipe régie +ccccc : prépa conf diversité et inclusivité pour OSXP +bbbbb : réflexion sur nouveau goody +mmmmm : Divers pour résultats Prix de science ouverte du logiciel libre de recherche +ccccc : point conf OSXP avec mmmmm +mmmmm : Récupération de l'Expolibre utilisée par la Cie Terraquée à Paris 8 +ccccc : point hebdo avec mmmmm +mmmmm : Divers pour AG (statuts, conférence d'ouverture) +ccccc : divers paie novembre +mmmmm : Divers pour avenir April, suivi possibles bénévoles, diversité et inclusivité etc +mmmmm : Validation de la revue de presse +mmmmm : Tâches récurrentes +mmmmm : %fini +ccccc : off mercredi 29 novembre +ccccc : %fini +lllll : % bbbbb fini ? +bbbbb : %fini +lllll : % on va passer à la suite : Actions à venir +lllll : # 2/ Actions à venir +lllll : participation réunion jury TNL le 4/12, finir lecture dossier candidature +mmmmm : Présence soirée Cause Commune vendredi 1er décembre à partir de 19 h 30 +ccccc : création badges pour bénévoles OSXP +bbbbb : écriture tour des gull +ooooo : % ras +lllll : présence OSXP 6 et 7 décembre (au stand, conf, remise label TNL), animation table ronde collectivité +bbbbb : promotion Ada et mr zingemann +mmmmm : Lettres d'information : Finalisation et diffusion lettre d'information publique du 1er décembre +lllll : LAV : divers pour émission à venir (remplir la grille de la rentrée) +bbbbb : présence salon osxp +bbbbb : modération du pouet +ccccc : suite préparation conf OSXP et peut-être et possible répétition avec public +lllll : suivi dossier +mmmmm : % bbbbb tu as reçu mon courriel au sujet du livre ? (demande de citation) +lllll : prise de contact tel avec membre wikilibriste +mmmmm : Évènements : Suite préparation conf diversité/inclusivité à OSXP +ccccc : présence sur salon OSXP : stand, conférence, tour des orgas avec bbbbb +bbbbb : animation (ou pas) track politique / village du libre / stand +lllll : Tâches récurrentes et divers selon l'actu +lllll : %fini +bbbbb : tour des entreprises adhérentes de l'April +ccccc : fixer dates du prochain LEF +bbbbb : !oups +mmmmm : Libre à vous ! Investigation problème AntennaPod et flux RSS +ccccc : traitement courriels en souffrance +bbbbb : tour des entreprises adhérentes de l'April avec ccccc +ccccc : divers goodies +mmmmm : Évènements : Présence salon OSXP +ccccc : %fini +bbbbb : %fini +lllll : % mmmmm fini ? +bbbbb : %cliente.. désolée +mmmmm : Le reste en fonction du temps disponible +mmmmm : Possible absence cet aprem +mmmmm : %fini +lllll : %on va passer à la suite : Points de blocage / points en retard +lllll : ## 3/ Points de blocage / points en retard corrigés cette semaine +ccccc : %ras +lllll : %ras +ooooo : % ras +mmmmm : Firefox sur mon laptop utilise trop de ressources (CPU notamment), problème peut-être réglé mais pas sûr cf https://agir.april.org/issues/6276 +mmmmm : %fini +lllll : %on va passer à la suite : Points de blocage / points en retard existants à traiter +lllll : ## 4/ Points de blocage existants / points en retard à traiter +lllll : prise de contact tel avec membre wikilibriste +lllll : attaquer lecture "Géopolitique du numérique" +ccccc : impression nouvel autocollant +mmmmm : %ras +ccccc : %fini +ooooo : % ras +lllll : % on va passer à la suite : Points Forts et Points de Vigilance de la semaine +lllll : ## 5/ Points forts de la semaine +mmmmm : %ras +ccccc : moins de pluie +ccccc : %fini +ooooo : Une belle émission mardi 28/11 +lllll : plaisir à refaire une régie solo (ça faisait longtemps) +ccccc : un sujet animé par une nouvelle bénévole +lllll : % on va passer à la suite : Points de Vigilance de la semaine +ccccc : %fini +lllll : ## 6/ Points de vigilance de la semaine +ooooo : Souci avec les statistiques du site Libre à lire ! > 37 000 visites le 30 novembre ! +mmmmm : %ras +lllll : convalescence un peu longue, mais ça va bien mieux. Hâte de repasser au local +ccccc : %ras +ooooo : % fini +lllll : % on va passer à la suite : Points Forts et Points de Vigilance de la réunion +lllll : ## 7/ Points forts de la réunion +ooooo : %ras +mmmmm : %ras +lllll : % un peu long à lancer dis donc +lllll : %ra +lllll : %ras +ccccc : plus de monde que la dernière fois +ccccc : %fini +lllll : % on va passer à la suite : Points de Vigilance de la réunion +lllll : ## 8/ Points de vigilance de la réunion +lllll : Les clientes qui dérangent la présidente en pleine réunion +mmmmm : % lllll tu as fait les annonces twitter et mastodon ? +ccccc : baisse de 5 dégérs entre la petite salle de réu et le bureau +ccccc : %fini +lllll : % ah non, j'y penserai dorénavant +mmmmm : Penser à faire les annonces twitter et mastodon +mmmmm : %fini +ccccc : %oups +ccccc : baisse de 5 degrés entre la petite salle de réu et le bureau +ccccc : %fini +lllll : % Fin de la revue hebdomadaire, merci à vous +lllll : !fin diff --git a/tests/datas/reviewstats.csv b/tests/datas/reviewstats.csv new file mode 100644 index 0000000..cbfc995 --- /dev/null +++ b/tests/datas/reviewstats.csv @@ -0,0 +1,313 @@ +20110930-12h00 6 +20111007-12h00 6 +20111014-12h00 8 +20111021-12h00 5 +20111028-12h00 10 +20111104-12h00 8 +20111111-12h00 3 +20111118-12h00 7 +20111125-12h00 7 +20111202-12h00 7 +20111209-12h00 3 +20111216-12h00 7 +20111223-12h00 7 +20111230-12h00 7 +20120106-12h00 8 +20120113-12h00 6 +20120120-12h00 7 +20120217-12h00 8 +20120302-12h00 10 +20120309-12h00 8 +20120316-12h00 3 +20120323-12h00 6 +20120330-12h00 6 +20120406-12h00 7 +20120413-12h00 7 +20120420-12h00 5 +20120504-12h00 8 +20120511-12h00 6 +20120518-12h00 4 +20120525-12h00 7 +20120601-12h00 3 +20120608-12h00 6 +20120615-12h00 7 +20120622-12h00 9 +20120629-12h00 4 +20120706-12h00 6 +20120720-12h00 8 +20120727-12h00 5 +20120803-12h00 6 +20120810-12h00 6 +20120817-12h00 6 +20120824-12h00 8 +20120831-12h00 10 +20120907-12h00 12 +20120914-12h00 8 +20120921-12h00 4 +20120928-12h00 8 +20121012-12h00 6 +20121019-12h00 7 +20121026-12h00 7 +20121102-12h00 8 +20121109-12h00 7 +20121116-12h00 8 +20121123-12h00 5 +20121130-12h00 13 +20121207-12h00 10 +20121214-12h00 14 +20121221-12h00 10 +20121228-12h00 4 +20130104-12h00 7 +20130111-12h00 6 +20130118-12h00 10 +20130125-12h00 10 +20130201-12h00 8 +20130208-12h00 11 +20130215-12h00 6 +20130222-12h00 7 +20130301-12h00 8 +20130308-12h00 4 +20130315-12h00 6 +20130322-12h00 4 +20130329-12h00 7 +20130405-12h00 10 +20130412-12h00 8 +20130419-12h00 6 +20130426-12h00 5 +20130503-12h00 8 +20130510-12h00 3 +20130517-12h00 8 +20130524-12h00 9 +20130531-12h00 7 +20130607-12h00 8 +20130614-12h00 9 +20130628-12h00 9 +20130705-12h00 9 +20130712-12h00 7 +20130719-12h00 11 +20130726-12h00 7 +20130802-12h00 11 +20130809-12h00 10 +20130816-12h00 6 +20130823-12h00 6 +20130830-12h00 6 +20130906-12h00 11 +20130913-12h00 8 +20130920-12h00 6 +20130927-12h00 6 +20131004-12h00 3 +20131011-12h00 5 +20131018-12h00 5 +20131025-12h00 3 +20131101-12h00 6 +20131108-12h00 4 +20131115-12h00 3 +20131122-12h00 6 +20131129-12h00 2 +20131206-12h00 6 +20131213-12h00 5 +20131220-12h00 2 +20131227-12h00 5 +20140103-12h00 4 +20140110-12h00 5 +20140117-12h00 8 +20140124-12h00 6 +20140131-12h00 7 +20140207-12h00 5 +20140214-12h00 6 +20140221-12h00 6 +20140228-12h00 8 +20140307-12h00 5 +20140314-12h00 6 +20140321-12h00 5 +20140328-12h00 6 +20140404-12h00 6 +20140411-12h00 5 +20140418-12h00 6 +20140425-12h00 5 +20140502-12h00 5 +20140509-12h00 6 +20140516-12h00 4 +20140523-12h00 2 +20140613-12h00 6 +20140620-12h00 4 +20140627-12h00 5 +20140704-12h00 6 +20140711-12h00 7 +20140718-12h00 7 +20140725-12h00 3 +20140801-12h00 6 +20140808-12h00 5 +20140822-12h00 4 +20140829-12h00 7 +20140905-12h00 6 +20140912-12h00 5 +20140919-12h00 5 +20140926-12h00 3 +20141010-12h00 5 +20141017-12h00 7 +20141024-12h00 6 +20141031-12h00 6 +20141107-12h00 3 +20141114-12h00 7 +20141121-12h00 7 +20141128-12h00 6 +20141205-12h00 7 +20141212-12h00 7 +20141219-12h00 6 +20150109-12h00 4 +20150116-12h00 1 +20150123-12h00 4 +20150130-12h00 6 +20150206-12h00 4 +20150213-12h00 2 +20150220-12h00 7 +20150227-12h00 4 +20150306-12h00 11 +20150313-12h00 10 +20150320-12h00 8 +20150327-12h00 5 +20150403-12h00 6 +20150410-12h00 8 +20150417-12h00 4 +20150424-12h00 3 +20150507-12h00 5 +20150515-12h00 8 +20150522-12h00 7 +20150529-12h00 5 +20150605-12h00 10 +20150612-12h00 6 +20150619-12h00 3 +20150626-12h00 5 +20150703-12h00 5 +20150710-12h00 5 +20150717-12h00 6 +20150724-12h00 7 +20150731-12h00 5 +20150807-12h00 4 +20150814-12h00 4 +20150828-12h00 5 +20150904-12h00 5 +20150918-12h00 6 +20150925-12h00 5 +20151002-12h00 7 +20151009-12h00 5 +20151016-12h00 8 +20151023-12h00 6 +20151030-12h00 8 +20151106-12h00 5 +20151113-12h00 9 +20151120-12h00 9 +20151127-12h00 7 +20151204-12h00 10 +20151211-12h00 7 +20151218-12h00 10 +20160108-12h00 8 +20160219-12h00 6 +20160226-12h00 9 +20160318-12h00 6 +20160401-12h00 6 +20160408-12h00 8 +20160415-12h00 6 +20160422-12h00 7 +20160429-12h00 7 +20160506-12h00 7 +20160513-12h00 6 +20160520-12h00 6 +20160527-12h00 8 +20160603-12h00 3 +20160610-12h00 4 +20160617-12h00 7 +20160624-12h00 6 +20160701-12h00 6 +20160708-12h00 7 +20160715-12h00 4 +20160722-12h00 6 +20160805-12h00 4 +20160812-12h00 3 +20160819-12h00 4 +20160826-12h00 4 +20160902-12h00 6 +20160909-12h00 7 +20160916-12h00 7 +20160923-12h00 7 +20160930-12h00 4 +20161007-12h00 6 +20161021-12h00 7 +20161028-12h00 6 +20161104-12h00 7 +20161110-12h00 7 +20161118-12h00 7 +20161125-12h00 7 +20161202-12h00 4 +20161209-12h00 7 +20161216-12h00 9 +20161223-12h00 5 +20170106-12h00 7 +20170113-12h00 4 +20170120-12h00 8 +20170127-12h00 7 +20170203-12h00 6 +20170210-12h00 6 +20170217-12h00 8 +20170224-12h00 8 +20170303-12h00 7 +20170310-12h00 7 +20170317-12h00 4 +20170324-12h00 6 +20170331-12h00 5 +20170407-12h00 4 +20170414-12h00 4 +20170421-12h00 9 +20170428-12h00 7 +20170505-12h00 6 +20170512-12h00 7 +20170519-12h00 6 +20170526-12h00 5 +20170602-12h00 8 +20170609-12h00 10 +20170616-12h00 9 +20170623-12h00 7 +20170630-12h00 5 +20170707-12h00 12 +20170713-12h00 4 +20170721-12h00 6 +20170727-12h00 6 +20170818-12h00 5 +20170825-12h00 3 +20170901-12h00 6 +20170908-12h00 5 +20170915-12h00 7 +20170922-12h00 5 +20170929-12h00 10 +20171006-12h00 9 +20171013-12h00 9 +20171020-12h00 7 +20171027-12h00 8 +20171103-12h00 10 +20171110-12h00 10 +20171117-12h00 8 +20171124-12h00 8 +20171201-12h00 9 +20171208-12h00 10 +20171215-12h00 11 +20171222-12h00 7 +20180105-12h00 7 +20180112-12h00 10 +20180118-12h00 1 +20180119-12h00 12 +20180126-12h00 10 +20180202-12h00 9 +20180209-12h00 8 17 +20180216-12h00 8 16 +20180223-12h00 5 14 +20180302-12h00 6 19 +20180309-12h00 11 20 +20180316-12h00 9 19 +20180323-12h00 9 17 +20180330-12h00 10 19 +20180406-12h00 9 17 +20180413-12h00 11 15 +20180420-12h00 6 17 +20180427-12h00 7 17 +20180504-12h00 7 17 diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..9338803 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,34 @@ +import logging +import os + + +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + +VERSION = "3.0.0" + +# Log File +LOGFILE = os.environ.get("LOGFILE", os.path.join(BASE_DIR, "hebdobot.log")) +LOGLEVEL = os.environ.get("LOGLEVEL", logging.DEBUG) +LOGFORMAT = os.environ.get("LOGFORMAT", "%(asctime)s.%(msecs)03d - %(message)s") +LOGDATE = os.environ.get("LOGDATE", "%Y-%m-%d %H:%M:%S") + +# IRC configuration +IRC_SERVER = os.environ.get("IRC_SERVER", "irc.libera.chat") +IRC_PORT = int(os.environ.get("IRC_PORT", 6667)) +IRC_CHANNEL = os.environ.get("IRC_CHANNEL", " #testbot") +IRC_NICK = os.environ.get("IRC_NICK", " Testbot") +IRC_PASSWORD = os.environ.get("IRC_PASSWORD", " ") +IRC_DELAY = float(os.environ.get("IRC_DELAY", 0.5)) # Délai entre plusieurs messages + +# User aliases +USER_ALIASES = os.environ.get("USER_ALIASES", os.path.join(BASE_DIR, "users.conf")) + +# Review +REVIEW_DIRECTORY = os.environ.get("REVIEW_DIRECTORY", os.path.join(BASE_DIR, "reviews")) +REVIEW_PASTEBIN = "" +REVIEW_STATS = os.environ.get("REVIEW_STATS", os.path.join(REVIEW_DIRECTORY, "reviewstats.csv")) + +# Mastodon +MASTODON_SERVER = "" +MASTODON_NAME = "" +MASTODON_PASSWORD = "" diff --git a/tests/test_aliases.py b/tests/test_aliases.py new file mode 100644 index 0000000..40a2397 --- /dev/null +++ b/tests/test_aliases.py @@ -0,0 +1,9 @@ +from bot.review.aliases import Aliases +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def test_aliases(bot): + aliases = Aliases(bot.settings.USER_ALIASES) + assert aliases["Test"] == "Real Name" + assert aliases["TEST"] == "Real Name" + assert aliases["test"] == "Real Name" diff --git a/tests/test_basic_hooks.py b/tests/test_basic_hooks.py new file mode 100644 index 0000000..aba08d5 --- /dev/null +++ b/tests/test_basic_hooks.py @@ -0,0 +1,124 @@ +import os +import shutil + +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def setup_function(): + shutil.copyfile("tests/datas/reviewstats.csv", "tests/reviews/reviewstats.csv") + + +def test_hook_listen_anniv(bot): + bot.on_public_message(CHANNEL, SENDER, "!anniv") + assert len(bot.answers) == 3 + assert bot.answers[0].message.startswith("La revue") + assert bot.answers[1].message.startswith("Hebdobot") + assert bot.answers[2].message.startswith("L'April") + + +def test_hook_bad_command(bot): + bot.on_public_message(CHANNEL, SENDER, "!aniv") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, Yo !" + + +def test_hook_date(bot): + bot.on_public_message(CHANNEL, SENDER, "!date") + assert len(bot.answers) == 1 + bot.on_public_message(CHANNEL, SENDER, "!time") + assert len(bot.answers) == 1 + bot.on_public_message(CHANNEL, SENDER, "!now") + assert len(bot.answers) == 1 + + +def test_hook_default(bot): + bot.on_public_message(CHANNEL, SENDER, "some message") + assert bot.answers == [] + + +def test_hook_hello(bot): + bot.on_public_message(CHANNEL, SENDER, "!salut") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, bonjour \\o/" + bot.on_public_message(CHANNEL, SENDER, "!bonjour") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, bonjour \\o/" + bot.on_public_message(CHANNEL, SENDER, "!hello") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, bonjour \\o/" + bot.on_public_message(CHANNEL, SENDER, "bonjour hebdobot") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, bonjour \\o/" + + +def test_hook_help(bot): + bot.on_public_message(CHANNEL, SENDER, "!help") + assert len(bot.answers) == 15 + bot.on_public_message(CHANNEL, SENDER, "!aide") + assert len(bot.answers) == 15 + + +def test_hook_license(bot): + bot.on_public_message(CHANNEL, SENDER, "!license") + assert len(bot.answers) == 1 + assert bot.answers[0].message.startswith("Hebdobot est un logiciel libre") + bot.on_public_message(CHANNEL, SENDER, "!licence") + assert len(bot.answers) == 1 + assert bot.answers[0].message.startswith("Hebdobot est un logiciel libre") + + +def test_hook_listen_alexandrie(bot): + bot.on_public_message(CHANNEL, "alexandrie", "!version") + assert bot.answers == [] + bot.on_public_message(CHANNEL, SENDER, "!version") + assert bot.answers != [] + + +def test_hook_record(bot): + bot.on_public_message(CHANNEL, SENDER, "!record") + assert len(bot.answers) == 1 + assert bot.answers[0].message.startswith("Le record de participation") + + +def test_hook_stats(bot): + bot.on_public_message(CHANNEL, SENDER, "!stats") + assert len(bot.answers) == 5 + assert bot.answers[0].message.startswith("% Il y a eu ") + + +def test_hook_stats_with_no_review(bot): + os.remove("tests/reviews/reviewstats.csv") + bot.on_public_message(CHANNEL, SENDER, "!stats") + assert len(bot.answers) == 5 + assert bot.answers[0].message == "% Il n'y a pas encore eu de revue." + + +def test_hook_status(bot): + bot.on_public_message(CHANNEL, SENDER, "!status") + assert len(bot.answers) == 3 + assert bot.answers[0].message.startswith(f"{SENDER}, voici l'état") + bot.on_public_message(CHANNEL, SENDER, "!statut") + assert len(bot.answers) == 3 + assert bot.answers[0].message.startswith(f"{SENDER}, voici l'état") + assert bot.answers[1].message == f" revue en cours : {bot.review is not None}" + assert bot.answers[2].message == " animateur revue : none" + + +def test_hook_status_started_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "!statut") + assert len(bot.answers) == 3 + assert bot.answers[0].message.startswith(f"{SENDER}, voici l'état") + assert bot.answers[1].message == f" revue en cours : {bot.review is not None}" + assert bot.answers[2].message == f" animateur revue : {OWNER}" + + +def test_hook_thanks(bot): + bot.on_public_message(CHANNEL, SENDER, "!merci") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, de rien \\o/" + + +def test_hook_version(bot): + bot.on_public_message(CHANNEL, SENDER, "!version") + assert len(bot.answers) == 1 diff --git a/tests/test_bot.py b/tests/test_bot.py new file mode 100644 index 0000000..14813ae --- /dev/null +++ b/tests/test_bot.py @@ -0,0 +1,12 @@ +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def test_private_message(bot): + bot.on_private_message(CHANNEL, SENDER, "hello") + assert len(bot.answers) == 0 + + +def test_private_command(bot): + bot.on_private_message(CHANNEL, SENDER, "!hello") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "vos commandes dans le salon public" diff --git a/tests/test_complete_review.py b/tests/test_complete_review.py new file mode 100644 index 0000000..65081ae --- /dev/null +++ b/tests/test_complete_review.py @@ -0,0 +1,31 @@ +from datetime import datetime +import shutil + +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def setup_function(): + shutil.copyfile("tests/datas/reviewstats.csv", "tests/reviews/reviewstats.csv") + + +def test_complete_review(bot): + with open("tests/datas/irc.txt") as file_handle: + content = file_handle.read().splitlines() + messages = [] + for line in content: + data = line.split(":") + author = data[0].strip() + text = ":".join(data[1:]).strip() + messages.append((author, text)) + + bot.on_public_message(CHANNEL, "lllll", "!start") + bot.review.start_time = datetime(2023, 12, 1, 12, 0, 0) + for message in messages: + bot.on_public_message(CHANNEL, message[0], message[1]) + + # compare contents + with open("tests/datas/20231201-log-irc-revue-hebdomadaire.txt") as file_handle: + content_ok = file_handle.read() + with open("tests/reviews/20231201-log-irc-revue-hebdomadaire.txt") as file_handle: + content_tested = file_handle.read() + assert content_ok in content_tested diff --git a/tests/test_review_hooks.py b/tests/test_review_hooks.py new file mode 100644 index 0000000..067489f --- /dev/null +++ b/tests/test_review_hooks.py @@ -0,0 +1,507 @@ +from datetime import datetime, timedelta +import shutil + +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def setup_function(): + shutil.copyfile("tests/datas/reviewstats.csv", "tests/reviews/reviewstats.csv") + + +def test_review_starting_new(bot): + assert bot.review.participants == [] + assert bot.review.topics == [] + assert bot.review.messages == [] + assert bot.review.owner is None + assert bot.review.started == False + assert bot.review.ended == False + assert bot.review.start_time is None + assert bot.review.end_time is None + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.answers) == 5 + assert bot.answers[0].target == OWNER + assert bot.answers[0].message.startswith(f"% Bonjour {OWNER}") + assert bot.answers[1].target == OWNER + assert bot.answers[1].message.startswith("% Pour terminer") + assert bot.answers[2].target == CHANNEL + assert bot.answers[2].message.startswith("% Début de") + assert bot.answers[3].target == CHANNEL + assert bot.answers[3].message.startswith("% rappel") + assert bot.answers[4].target == CHANNEL + assert bot.answers[4].message.startswith("% pour connaître") + assert bot.review.participants == [] + assert bot.review.topics == [] + assert len(bot.review.messages) == 3 + assert bot.review.owner == OWNER + assert bot.review.started == True + assert bot.review.ended == False + assert bot.review.start_time is not None + assert bot.review.end_time is None + + +def test_review_starting_started_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"% {OWNER}, une revue est déjà en cours." + assert bot.review.participants == [] + assert bot.review.topics == [] + assert bot.review.messages[-1].text == f"% {OWNER}, une revue est déjà en cours." + assert bot.review.owner == OWNER + assert bot.review.started == True + assert bot.review.ended == False + assert bot.review.start_time is not None + assert bot.review.end_time is None + + +def test_review_ending_not_started_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{OWNER}, pas de revue en cours." + assert bot.review.participants == [] + assert bot.review.topics == [] + assert bot.review.messages == [] + assert bot.review.owner is None + assert bot.review.started == False + assert bot.review.ended == False + assert bot.review.start_time is None + assert bot.review.end_time is None + + +def test_review_ending_as_not_owner(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, vous n'êtes pas responsable de la revue." + assert bot.review.participants == [] + assert bot.review.topics == [] + assert bot.review.messages[-1].text == f"{SENDER}, vous n'êtes pas responsable de la revue." + assert bot.review.owner == OWNER + assert bot.review.started == True + assert bot.review.ended == False + assert bot.review.start_time is not None + assert bot.review.end_time is None + + +def test_review_cancel_last_message_on_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## Collective topic") + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.review.current_topic.messages) == 0 + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oops") + assert len(bot.review.current_topic.messages) == 0 + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!cancelprevious") + assert len(bot.review.current_topic.messages) == 0 + + +def test_review_cancel_only_message_on_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## Collective topic") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "This is another message from owner") + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 2 + assert bot.review.current_topic.messages[1].text == "This is another message from owner" + + +def test_review_cancel_last_message_from_two_on_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## Collective topic") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, SENDER, "This is another message from sender") + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 2 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + + +def test_review_cancel_first_message_from_three_on_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## Collective topic") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, OWNER, "This is another message from owner") + bot.on_public_message(CHANNEL, OWNER, "This is a third message from owner") + assert len(bot.review.current_topic.messages) == 4 + assert bot.review.current_topic.messages[1].text == "This is a message from owner" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is another message from owner" + + +def test_review_cancel_last_message_no_message_on_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## Collective topic") + bot.on_public_message(CHANNEL, OWNER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + + +def test_review_cancel_last_message_on_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# Individual topic") + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.review.current_topic.messages) == 0 + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oops") + assert len(bot.review.current_topic.messages) == 0 + bot.on_public_message(CHANNEL, SENDER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!cancelprevious") + assert len(bot.review.current_topic.messages) == 0 + + +def test_review_cancel_only_message_on_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# Individual topic") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "This is another message from owner") + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 2 + assert bot.review.current_topic.messages[1].text == "This is another message from owner" + + +def test_review_cancel_last_message_from_two_on_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# Individual topic") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, SENDER, "This is another message from sender") + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 2 + assert bot.review.current_topic.messages[1].text == "This is a message from sender" + + +def test_review_cancel_first_message_from_three_on_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# Individual topic") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "This is a message from owner") + bot.on_public_message(CHANNEL, OWNER, "This is another message from owner") + bot.on_public_message(CHANNEL, OWNER, "This is a third message from owner") + assert len(bot.review.current_topic.messages) == 4 + assert bot.review.current_topic.messages[1].text == "This is a message from owner" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert len(bot.review.current_topic.messages) == 3 + assert bot.review.current_topic.messages[1].text == "This is another message from owner" + + +def test_review_cancel_last_message_no_message_on_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# Individual topic") + bot.on_public_message(CHANNEL, OWNER, "This is my message") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.review.current_topic.messages) == 1 + assert bot.review.current_topic.messages[0].text == "This is my message" + + +def test_review_cancel_last_message_no_review(bot): + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, pas de revue en cours." + + +def test_review_cancel_last_message_no_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "!oups") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, pas de sujet en cours." + + +def test_review_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + assert len(bot.answers) == 4 + assert len(bot.review.topics) == 1 + assert bot.review.current_topic is not None + assert bot.review.current_topic.collective + + +def test_review_second_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "This is my message") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "## second collective topic") + assert len(bot.answers) == 6 + assert len(bot.review.topics) == 2 + assert bot.review.current_topic is not None + assert bot.review.current_topic.collective + + +def test_review_collective_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + assert len(bot.answers) == 0 + assert not bot.review.is_started + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + + +def test_review_collective_topic_not_owner(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "## collective topic") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, vous n'êtes pas responsable de la réunion" + + +def test_review_comment(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "% This is a comment") + assert len(bot.answers) == 0 + assert bot.review.messages[-1].text == "% This is a comment" + assert bot.review.current_topic is None + + +def test_review_current_without_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!courant") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{OWNER}, pas de revue en cours." + assert not bot.review.is_started + + +def test_review_current_without_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "!courant") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "% Pas de sujet en cours." + assert bot.review.is_started + + +def test_review_current_with_collective_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "!courant") + assert len(bot.answers) == 1 + assert bot.review.current_topic.title == "collective topic" + assert bot.answers[0].message == f"% Sujet collectif en cours : {bot.review.current_topic.title}" + + +def test_review_current_with_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + bot.on_public_message(CHANNEL, OWNER, "!courant") + assert len(bot.answers) == 1 + assert bot.review.current_topic.title == "individual topic" + assert bot.answers[0].message == f"% Sujet individuel en cours : {bot.review.current_topic.title}" + + +def test_review_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + assert len(bot.answers) == 3 + assert len(bot.review.topics) == 1 + assert bot.review.current_topic is not None + assert bot.review.current_topic.individual + + +def test_review_second_individual_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + bot.on_public_message(CHANNEL, OWNER, "This is my message") + bot.on_public_message(CHANNEL, SENDER, "This is a message from sender") + bot.on_public_message(CHANNEL, OWNER, "# second individual topic") + assert len(bot.answers) == 5 + assert len(bot.review.topics) == 2 + assert bot.review.current_topic is not None + assert bot.review.current_topic.individual + + +def test_review_individual_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + assert len(bot.answers) == 0 + assert not bot.review.is_started + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + + +def test_review_individual_topic_not_owner(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "# individual topic") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, vous n'êtes pas responsable de la réunion" + + +def test_stop_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert bot.review.is_started + assert not bot.review.is_ended + bot.on_public_message(CHANNEL, OWNER, "A messsage") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "Another messsage") + bot.on_public_message(CHANNEL, OWNER, "!stop") + assert not bot.review.is_started + assert not bot.review.is_ended + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + assert len(bot.answers) == 1 + assert bot.answers[0].message == "Abandon de la revue en cours." + + +def test_stop_review_not_owner(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, SENDER, "!stop") + assert bot.review.is_started + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, vous n'êtes pas responsable de la réunion." + + +def test_stop_review_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!stop") + assert not bot.review.is_started + assert not bot.review.is_ended + assert len(bot.review.topics) == 0 + assert bot.review.current_topic is None + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{OWNER}, pas de revue en cours, abandon impossible." + + +def test_missing_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!manquants") + assert not bot.review.is_started + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{OWNER}, pas de revue en cours." + + +def test_missing_no_topic(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + assert bot.review.is_started + bot.on_public_message(CHANNEL, OWNER, "!manquants") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "% Pas de sujet en cours." + + +def test_missing_no_one_is_missing(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, SENDER, "This is my message") + bot.on_public_message(CHANNEL, OWNER, "This is owner message") + bot.on_public_message(CHANNEL, OWNER, "!manquants") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "% Tout le monde s'est exprimé sur le sujet courant \\o/" + + +def test_missing_one_is_missing(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, SENDER, "This is my message") + bot.on_public_message(CHANNEL, OWNER, "This is owner message") + bot.on_public_message(CHANNEL, OWNER, "## a second collective topic") + bot.on_public_message(CHANNEL, OWNER, "This is the second owner message") + bot.on_public_message(CHANNEL, OWNER, "!manquants") + assert len(bot.answers) == 1 + assert bot.answers[0].message == ( + "% Personnes participantes ne s'étant pas exprimées sur le " + f"sujet courant : {', '.join([SENDER])}" + ) + + +def test_chrono_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!chrono") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "n/a" + + +def test_chrono(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.review.start_time = datetime.today() - timedelta(seconds=87) + bot.on_public_message(CHANNEL, OWNER, "!chrono") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "01:27" + + +def test_finish_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on collective topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on collective topic") + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on individual topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on individual topic") + bot.on_public_message(CHANNEL, OWNER, "!fin") + assert len(bot.answers) == 10 + assert bot.answers[-2].message == "% Fin de la revue hebdomadaire" + assert bot.answers[-1].message == "Revue finie." + + +def test_finish_review_no_review(bot): + bot.on_public_message(CHANNEL, OWNER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{OWNER}, pas de revue en cours." + + +def test_finish_review_already_finished(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on collective topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on collective topic") + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on individual topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on individual topic") + bot.on_public_message(CHANNEL, OWNER, "!fin") + bot.on_public_message(CHANNEL, OWNER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "La revue est déjà finie." + + +def test_finish_review_not_owner(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on collective topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on collective topic") + bot.on_public_message(CHANNEL, OWNER, "# individual topic") + bot.on_public_message(CHANNEL, OWNER, "Owner message on individual topic") + bot.on_public_message(CHANNEL, SENDER, "Sender message on individual topic") + bot.on_public_message(CHANNEL, SENDER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == f"{SENDER}, vous n'êtes pas responsable de la revue." + + +def test_finish_review_no_participation(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "!fin") + assert len(bot.answers) == 1 + assert bot.answers[0].message == "Participation nulle détectée. La revue est ignorée." diff --git a/tests/test_topic.py b/tests/test_topic.py new file mode 100644 index 0000000..7637317 --- /dev/null +++ b/tests/test_topic.py @@ -0,0 +1,8 @@ +from tests.utils import bot, CHANNEL, OWNER, SENDER + + +def test_topic_duration_not_ended(bot): + bot.on_public_message(CHANNEL, OWNER, "!start") + bot.on_public_message(CHANNEL, OWNER, "## collective topic") + assert bot.review.current_topic.duration != "" + diff --git a/tests/users.conf b/tests/users.conf new file mode 100644 index 0000000..17ad81e --- /dev/null +++ b/tests/users.conf @@ -0,0 +1,10 @@ +# +# Sample Hebdobot user file +# + +Mmmmm Mmmmm=mmmmm +Ccccc Ccccc=ccccc +Lllll Lllll=lllll +Ooooo Ooooo=ooooo,ooooo1 +Bbbbb Bbbbb=bbbbb +Real Name=Test diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..2b4561a --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,13 @@ +import pytest + +from bot.hebdobot import Hebdobot +from tests import settings + +CHANNEL = "#test_channel" +OWNER = "me" +SENDER = "foobar" + + +@pytest.fixture +def bot(): + return Hebdobot(settings, CHANNEL, nickname="Hebdobot") \ No newline at end of file