From 3058a53934b5ad38e1dd4e03594481585b2224e6 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2019 09:56:08 +0100 Subject: [PATCH] First comit --- .gitignore | 13 + LICENSE.txt | 661 ++++++++++++++++++++++++++ README.md | 65 +++ requirements.txt | 7 + royalpack/__init__.py | 17 + royalpack/commands/__init__.py | 58 +++ royalpack/commands/ciaoruozi.py | 22 + royalpack/commands/color.py | 12 + royalpack/commands/cv.py | 130 +++++ royalpack/commands/diario.py | 206 ++++++++ royalpack/commands/diarioquote.py | 25 + royalpack/commands/emojify.py | 73 +++ royalpack/commands/leagueoflegends.py | 224 +++++++++ royalpack/commands/matchmaking.py | 244 ++++++++++ royalpack/commands/mp3.py | 40 ++ royalpack/commands/pause.py | 53 +++ royalpack/commands/peertube.py | 81 ++++ royalpack/commands/play.py | 79 +++ royalpack/commands/playmode.py | 57 +++ royalpack/commands/queue.py | 93 ++++ royalpack/commands/rage.py | 20 + royalpack/commands/reminder.py | 86 ++++ royalpack/commands/ship.py | 40 ++ royalpack/commands/skip.py | 48 ++ royalpack/commands/smecds.py | 70 +++ royalpack/commands/soundcloud.py | 77 +++ royalpack/commands/summon.py | 77 +++ royalpack/commands/trivia.py | 139 ++++++ royalpack/commands/videochannel.py | 50 ++ royalpack/commands/youtube.py | 76 +++ royalpack/commands/zawarudo.py | 91 ++++ royalpack/stars/__init__.py | 21 + royalpack/stars/api_diario_get.py | 22 + royalpack/stars/api_diario_list.py | 25 + royalpack/stars/api_user_get.py | 22 + royalpack/stars/api_user_list.py | 15 + royalpack/tables/__init__.py | 35 ++ royalpack/tables/aliases.py | 28 ++ royalpack/tables/bios.py | 28 ++ royalpack/tables/diario.py | 105 ++++ royalpack/tables/leagueoflegends.py | 256 ++++++++++ royalpack/tables/mmevents.py | 52 ++ royalpack/tables/mmresponse.py | 31 ++ royalpack/tables/reminders.py | 46 ++ royalpack/tables/triviascores.py | 40 ++ royalpack/tables/wikipages.py | 38 ++ royalpack/tables/wikirevisions.py | 48 ++ royalpack/utils/__init__.py | 7 + royalpack/utils/leagueleague.py | 134 ++++++ royalpack/utils/leaguerank.py | 21 + royalpack/utils/leaguetier.py | 26 + royalpack/utils/mmchoice.py | 12 + royalpack/utils/mminterfacedata.py | 10 + royalpack/version.py | 4 + setup.py | 35 ++ to_pypi.bat | 4 + to_pypi.sh | 12 + 57 files changed, 4011 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 royalpack/__init__.py create mode 100644 royalpack/commands/__init__.py create mode 100644 royalpack/commands/ciaoruozi.py create mode 100644 royalpack/commands/color.py create mode 100644 royalpack/commands/cv.py create mode 100644 royalpack/commands/diario.py create mode 100644 royalpack/commands/diarioquote.py create mode 100644 royalpack/commands/emojify.py create mode 100644 royalpack/commands/leagueoflegends.py create mode 100644 royalpack/commands/matchmaking.py create mode 100644 royalpack/commands/mp3.py create mode 100644 royalpack/commands/pause.py create mode 100644 royalpack/commands/peertube.py create mode 100644 royalpack/commands/play.py create mode 100644 royalpack/commands/playmode.py create mode 100644 royalpack/commands/queue.py create mode 100644 royalpack/commands/rage.py create mode 100644 royalpack/commands/reminder.py create mode 100644 royalpack/commands/ship.py create mode 100644 royalpack/commands/skip.py create mode 100644 royalpack/commands/smecds.py create mode 100644 royalpack/commands/soundcloud.py create mode 100644 royalpack/commands/summon.py create mode 100644 royalpack/commands/trivia.py create mode 100644 royalpack/commands/videochannel.py create mode 100644 royalpack/commands/youtube.py create mode 100644 royalpack/commands/zawarudo.py create mode 100644 royalpack/stars/__init__.py create mode 100644 royalpack/stars/api_diario_get.py create mode 100644 royalpack/stars/api_diario_list.py create mode 100644 royalpack/stars/api_user_get.py create mode 100644 royalpack/stars/api_user_list.py create mode 100644 royalpack/tables/__init__.py create mode 100644 royalpack/tables/aliases.py create mode 100644 royalpack/tables/bios.py create mode 100644 royalpack/tables/diario.py create mode 100644 royalpack/tables/leagueoflegends.py create mode 100644 royalpack/tables/mmevents.py create mode 100644 royalpack/tables/mmresponse.py create mode 100644 royalpack/tables/reminders.py create mode 100644 royalpack/tables/triviascores.py create mode 100644 royalpack/tables/wikipages.py create mode 100644 royalpack/tables/wikirevisions.py create mode 100644 royalpack/utils/__init__.py create mode 100644 royalpack/utils/leagueleague.py create mode 100644 royalpack/utils/leaguerank.py create mode 100644 royalpack/utils/leaguetier.py create mode 100644 royalpack/utils/mmchoice.py create mode 100644 royalpack/utils/mminterfacedata.py create mode 100644 royalpack/version.py create mode 100644 setup.py create mode 100644 to_pypi.bat create mode 100755 to_pypi.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2ea4e4db --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +config.ini +.idea/ +.vscode/ +__pycache__ +downloads/ +ignored/ +markovmodels/ +logs/ +royalnet.egg-info/ +.pytest_cache/ +dist/ +build/ +venv/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/LICENSE.txt @@ -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 00000000..9361510c --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# `royalpack` [![PyPI](https://img.shields.io/pypi/v/royalpack.svg)](https://pypi.org/project/royalpack/) + +The Royalnet Pack used in the Royal Games community! + +## Commands + +### `ciaoruozi` + +### `color` + +### `cv` + +### `diario` + +### `rage` + +### `reminder` + +### `ship` + +### `smecds` + +### `videochannel` + +### `trivia` + +### `matchmaking` + +### `pause` + +### `play` + +### `playmode` + +### `queue` + +### `skip` + +### `summon` + +### `youtube` + +### `soundcloud` + +### `zawarudo` + +### `emojify` + +### `leagueoflegends` + +### `diarioquote` + +### `mp3` + +### `peertube` + +## Stars + +### `/api/diario/list` + +### `/api/diario/get/{id}` + +### `/api/user/list` + +### `/api/user/get/{id}` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ee0707bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +royalnet>=5.0 +starlette>=0.12.13 +aiohttp>=3.6.2 +sqlalchemy>=1.3.10 +dateparser>=0.7.2 +python_telegram_bot>=12.2.0 +discord.py>=1.3.0 diff --git a/royalpack/__init__.py b/royalpack/__init__.py new file mode 100644 index 00000000..aa4b1d96 --- /dev/null +++ b/royalpack/__init__.py @@ -0,0 +1,17 @@ +# This is a template Pack __init__. You can use this without changing anything in other packages too! + +from . import commands, tables, stars, version +from .commands import available_commands +from .tables import available_tables +from .stars import available_page_stars, available_exception_stars + +__all__ = [ + "commands", + "tables", + "stars", + "version", + "available_commands", + "available_tables", + "available_page_stars", + "available_exception_stars", +] diff --git a/royalpack/commands/__init__.py b/royalpack/commands/__init__.py new file mode 100644 index 00000000..01eeda13 --- /dev/null +++ b/royalpack/commands/__init__.py @@ -0,0 +1,58 @@ +# Imports go here! +from .ciaoruozi import CiaoruoziCommand +from .color import ColorCommand +from .cv import CvCommand +from .diario import DiarioCommand +from .rage import RageCommand +from .reminder import ReminderCommand +from .ship import ShipCommand +from .smecds import SmecdsCommand +from .videochannel import VideochannelCommand +from .trivia import TriviaCommand +from .matchmaking import MatchmakingCommand +from .pause import PauseCommand +from .play import PlayCommand +from .playmode import PlaymodeCommand +from .queue import QueueCommand +from .skip import SkipCommand +from .summon import SummonCommand +from .youtube import YoutubeCommand +from .soundcloud import SoundcloudCommand +from .zawarudo import ZawarudoCommand +from .emojify import EmojifyCommand +from .leagueoflegends import LeagueoflegendsCommand +from .diarioquote import DiarioquoteCommand +from .mp3 import Mp3Command +from .peertube import PeertubeCommand + +# Enter the commands of your Pack here! +available_commands = [ + CiaoruoziCommand, + ColorCommand, + CvCommand, + DiarioCommand, + RageCommand, + ReminderCommand, + ShipCommand, + SmecdsCommand, + VideochannelCommand, + TriviaCommand, + MatchmakingCommand, + PauseCommand, + PlayCommand, + PlaymodeCommand, + QueueCommand, + SkipCommand, + SummonCommand, + YoutubeCommand, + SoundcloudCommand, + ZawarudoCommand, + EmojifyCommand, + LeagueoflegendsCommand, + DiarioquoteCommand, + Mp3Command, + PeertubeCommand, +] + +# Don't change this, it should automatically generate __all__ +__all__ = [command.__name__ for command in available_commands] diff --git a/royalpack/commands/ciaoruozi.py b/royalpack/commands/ciaoruozi.py new file mode 100644 index 00000000..7deed002 --- /dev/null +++ b/royalpack/commands/ciaoruozi.py @@ -0,0 +1,22 @@ +import typing +import telegram +from royalnet.commands import * + + +class CiaoruoziCommand(Command): + name: str = "ciaoruozi" + + description: str = "Saluta Ruozi, un leggendario essere che una volta era in User Games." + + syntax: str = "" + + tables: typing.Set = set() + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name == "telegram": + update: telegram.Update = data.update + user: telegram.User = update.effective_user + if user.id == 112437036: + await data.reply("๐Ÿ‘‹ Ciao me!") + return + await data.reply("๐Ÿ‘‹ Ciao Ruozi!") diff --git a/royalpack/commands/color.py b/royalpack/commands/color.py new file mode 100644 index 00000000..a7432f45 --- /dev/null +++ b/royalpack/commands/color.py @@ -0,0 +1,12 @@ +from royalnet.commands import * + + +class ColorCommand(Command): + name: str = "color" + + description: str = "Invia un colore in chat...?" + + async def run(self, args: CommandArgs, data: CommandData) -> None: + await data.reply(""" + [i]I am sorry, unknown error occured during working with your request, Admin were notified[/i] + """) diff --git a/royalpack/commands/cv.py b/royalpack/commands/cv.py new file mode 100644 index 00000000..bf9f2b23 --- /dev/null +++ b/royalpack/commands/cv.py @@ -0,0 +1,130 @@ +import discord +import typing +from royalnet.commands import * +from royalnet.utils import andformat +from royalnet.bots import DiscordBot + + +class CvCommand(Command): + name: str = "cv" + + description: str = "Elenca le persone attualmente connesse alla chat vocale." + + syntax: str = "[guildname] [all]" + + @staticmethod + async def _legacy_cv_handler(bot: DiscordBot, guild_name: typing.Optional[str], everyone: bool): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("No guilds with the specified name found.") + if len(guilds) > 1: + raise CommandError("Multiple guilds with the specified name found.") + guild = list(bot.client.guilds)[0] + # Edit the message, sorted by channel + discord_members = list(guild.members) + channels = {0: None} + members_in_channels = {0: []} + message = "" + # Find all the channels + for member in discord_members: + if member.voice is not None: + channel = members_in_channels.get(member.voice.channel.id) + if channel is None: + members_in_channels[member.voice.channel.id] = list() + channel = members_in_channels[member.voice.channel.id] + channels[member.voice.channel.id] = member.voice.channel + channel.append(member) + else: + members_in_channels[0].append(member) + # Edit the message, sorted by channel + for channel in sorted(channels, key=lambda c: -c): + members_in_channels[channel].sort(key=lambda x: x.nick if x.nick is not None else x.name) + if channel == 0 and len(members_in_channels[0]) > 0: + message += "[b]Non in chat vocale:[/b]\n" + else: + message += f"[b]In #{channels[channel].name}:[/b]\n" + for member in members_in_channels[channel]: + member: typing.Union[discord.User, discord.Member] + # Ignore not-connected non-notable members + if not everyone and channel == 0 and len(member.roles) < 2: + continue + # Ignore offline members + if member.status == discord.Status.offline and member.voice is None: + continue + # Online status emoji + if member.bot: + message += "๐Ÿค– " + elif member.status == discord.Status.online: + message += "๐Ÿ”ต " + elif member.status == discord.Status.idle: + message += "โšซ " + elif member.status == discord.Status.dnd: + message += "๐Ÿ”ด " + elif member.status == discord.Status.offline: + message += "โšช " + # Voice + if channel != 0: + # Voice status + if member.voice.afk: + message += "๐Ÿ’ค " + elif member.voice.self_deaf or member.voice.deaf: + message += "๐Ÿ”‡ " + elif member.voice.self_mute or member.voice.mute: + message += "๐Ÿ”ˆ " + elif member.voice.self_video or member.voice.self_stream: + message += "๐Ÿ–ฅ " + else: + message += "๐Ÿ”Š " + # Nickname + # if member.nick is not None: + # message += f"[i]{member.nick}[/i]" + # else: + message += member.name + # Game or stream + if member.activity is not None: + if member.activity.type == discord.ActivityType.playing: + message += f" | ๐ŸŽฎ {member.activity.name}" + # Rich presence + try: + if member.activity.state is not None: + message += f" ({member.activity.state}" \ + f" | {member.activity.details})" + except AttributeError: + pass + elif member.activity.type == discord.ActivityType.streaming: + message += f" | ๐Ÿ“ก {member.activity.url}" + elif member.activity.type == discord.ActivityType.listening: + if isinstance(member.activity, discord.Spotify): + if member.activity.title == member.activity.album: + message += f" | ๐ŸŽง {member.activity.title} ({andformat(member.activity.artists, final=' e ')})" + else: + message += f" | ๐ŸŽง {member.activity.title} ({member.activity.album} | {andformat(member.activity.artists, final=' e ')})" + else: + message += f" | ๐ŸŽง {member.activity.name}" + elif member.activity.type == discord.ActivityType.watching: + message += f" | ๐Ÿ“บ {member.activity.name}" + else: + message += f" | โ“ {member.activity.state}" + message += "\n" + message += "\n" + return {"response": message} + + _event_name = "_legacy_cv" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_cv_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + # noinspection PyTypeChecker + guild_name, everyone = args.match(r"(?:\[(.+)])?\s*(\S+)?\s*") + response = await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "everyone": everyone + }) + await data.reply(response["response"]) diff --git a/royalpack/commands/diario.py b/royalpack/commands/diario.py new file mode 100644 index 00000000..fecc0b09 --- /dev/null +++ b/royalpack/commands/diario.py @@ -0,0 +1,206 @@ +import typing +import re +import datetime +import telegram +import aiohttp +from royalnet.commands import * +from royalnet.utils import asyncify +from ..tables import User, Diario, Alias + + +async def to_imgur(imgur_api_key, photosizes: typing.List[telegram.PhotoSize], caption="") -> str: + # Select the largest photo + largest_photo = sorted(photosizes, key=lambda p: p.width * p.height)[-1] + # Get the photo url + photo_file: telegram.File = await asyncify(largest_photo.get_file) + # Forward the url to imgur, as an upload + async with aiohttp.request("post", "https://api.imgur.com/3/upload", data={ + "image": photo_file.file_path, + "type": "URL", + "title": "Diario image", + "description": caption + }, headers={ + "Authorization": f"Client-ID {imgur_api_key}" + }) as request: + response = await request.json() + if not response["success"]: + raise CommandError("Imgur returned an error in the image upload.") + return response["data"]["link"] + + +class DiarioCommand(Command): + name: str = "diario" + + description: str = "Aggiungi una citazione al Diario." + + syntax = "[!] \"{testo}\" --[autore], [contesto]" + + tables = {User, Diario, Alias} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name == "telegram": + update: telegram.Update = data.update + message: telegram.Message = update.message + reply: telegram.Message = message.reply_to_message + creator = await data.get_author() + # noinspection PyUnusedLocal + quoted: typing.Optional[str] + # noinspection PyUnusedLocal + text: typing.Optional[str] + # noinspection PyUnusedLocal + context: typing.Optional[str] + # noinspection PyUnusedLocal + timestamp: datetime.datetime + # noinspection PyUnusedLocal + media_url: typing.Optional[str] + # noinspection PyUnusedLocal + spoiler: bool + if creator is None: + await data.reply("โš ๏ธ Devi essere registrato a Royalnet per usare questo comando!") + return + if reply is not None: + # Get the message text + text = reply.text + # Check if there's an image associated with the reply + photosizes: typing.Optional[typing.List[telegram.PhotoSize]] = reply.photo + if photosizes: + # Text is a caption + text = reply.caption + media_url = await to_imgur(self.interface.bot.get_secret("imgur"), + photosizes, text if text is not None else "") + else: + media_url = None + # Ensure there is a text or an image + if not (text or media_url): + raise InvalidInputError("Missing text.") + # Find the Royalnet account associated with the sender + quoted_tg = await asyncify(data.session.query(self.interface.alchemy.Telegram) + .filter_by(tg_id=reply.from_user.id) + .one_or_none) + quoted_account = quoted_tg.royal if quoted_tg is not None else None + # Find the quoted name to assign + quoted_user: telegram.User = reply.from_user + quoted = quoted_user.full_name + # Get the timestamp + timestamp = reply.date + # Set the other properties + spoiler = False + context = None + else: + # Get the current timestamp + timestamp = datetime.datetime.now() + # Get the message text + raw_text = " ".join(args) + # Check if there's an image associated with the reply + photosizes: typing.Optional[typing.List[telegram.PhotoSize]] = message.photo + if photosizes: + media_url = await to_imgur(self.interface.bot.get_secret("imgur"), + photosizes, raw_text if raw_text is not None else "") + else: + media_url = None + # Parse the text, if it exists + if raw_text: + # Pass the sentence through the diario regex + match = re.match( + r'(!)? *["ยซโ€˜โ€œโ€›โ€Ÿโ›โใ€๏ผ‚`]([^"]+)["ยปโ€™โ€โœโžใ€ž๏ผ‚`] *(?:(?:-{1,2}|โ€”) *([^,]+))?(?:, *([^ ].*))?', + raw_text) + # Find the corresponding matches + if match is not None: + spoiler = bool(match.group(1)) + text = match.group(2) + quoted = match.group(3) + context = match.group(4) + # Otherwise, consider everything part of the text + else: + spoiler = False + text = raw_text + quoted = None + context = None + # Ensure there's a quoted + if not quoted: + quoted = None + if not context: + context = None + # Find if there's a Royalnet account associated with the quoted name + if quoted is not None: + quoted_alias = await asyncify( + data.session.query(self.interface.alchemy.Alias) + .filter_by(alias=quoted.lower()).one_or_none) + else: + quoted_alias = None + quoted_account = quoted_alias.royal if quoted_alias is not None else None + else: + text = None + quoted = None + quoted_account = None + spoiler = False + context = None + # Ensure there is a text or an image + if not (text or media_url): + raise InvalidInputError("Missing text.") + # Create the diario quote + diario = self.interface.alchemy.Diario(creator=creator, + quoted_account=quoted_account, + quoted=quoted, + text=text, + context=context, + timestamp=timestamp, + media_url=media_url, + spoiler=spoiler) + data.session.add(diario) + await asyncify(data.session.commit) + await data.reply(f"โœ… {str(diario)}") + else: + # Find the creator of the quotes + creator = await data.get_author(error_if_none=True) + # Recreate the full sentence + raw_text = " ".join(args) + # Pass the sentence through the diario regex + match = re.match(r'(!)? *["ยซโ€˜โ€œโ€›โ€Ÿโ›โใ€๏ผ‚`]([^"]+)["ยปโ€™โ€โœโžใ€ž๏ผ‚`] *(?:(?:-{1,2}|โ€”) *([^,]+))?(?:, *([^ ].*))?', + raw_text) + # Find the corresponding matches + if match is not None: + spoiler = bool(match.group(1)) + text = match.group(2) + quoted = match.group(3) + context = match.group(4) + # Otherwise, consider everything part of the text + else: + spoiler = False + text = raw_text + quoted = None + context = None + timestamp = datetime.datetime.now() + # Ensure there is some text + if not text: + raise InvalidInputError("Missing text.") + # Or a quoted + if not quoted: + quoted = None + if not context: + context = None + # Find if there's a Royalnet account associated with the quoted name + if quoted is not None: + quoted_alias = await asyncify( + data.session.query(self.interface.alchemy.Alias) + .filter_by(alias=quoted.lower()) + .one_or_none) + else: + quoted_alias = None + quoted_account = quoted_alias.royal if quoted_alias is not None else None + if quoted_alias is not None and quoted_account is None: + await data.reply("โš ๏ธ Il nome dell'autore รจ ambiguo, quindi la riga non รจ stata aggiunta.\n" + "Per piacere, ripeti il comando con un nome piรน specifico!") + return + # Create the diario quote + diario = self.interface.alchemy.Diario(creator=creator, + quoted_account=quoted_account, + quoted=quoted, + text=text, + context=context, + timestamp=timestamp, + media_url=None, + spoiler=spoiler) + data.session.add(diario) + await asyncify(data.session.commit) + await data.reply(f"โœ… {str(diario)}") diff --git a/royalpack/commands/diarioquote.py b/royalpack/commands/diarioquote.py new file mode 100644 index 00000000..adaf74f1 --- /dev/null +++ b/royalpack/commands/diarioquote.py @@ -0,0 +1,25 @@ +from royalnet.commands import * +from royalnet.utils import * +from ..tables import Diario + + +class DiarioquoteCommand(Command): + name: str = "diarioquote" + + description: str = "Cita una riga del diario." + + aliases = ["dq", "quote", "dquote"] + + syntax = "{id}" + + tables = {Diario} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + try: + entry_id = int(args[0].lstrip("#")) + except ValueError: + raise CommandError("L'id che hai specificato non รจ valido.") + entry: Diario = await asyncify(data.session.query(self.alchemy.Diario).get, entry_id) + if entry is None: + raise CommandError("Nessuna riga con quell'id trovata.") + await data.reply(str(entry)) diff --git a/royalpack/commands/emojify.py b/royalpack/commands/emojify.py new file mode 100644 index 00000000..acd7177e --- /dev/null +++ b/royalpack/commands/emojify.py @@ -0,0 +1,73 @@ +import random +from royalnet.commands import * + + +class EmojifyCommand(Command): + name: str = "emojify" + + description: str = "Converti un messaggio in emoji." + + syntax = "{messaggio}" + + _emojis = { + "abcd": ["๐Ÿ”ก", "๐Ÿ” "], + "back": ["๐Ÿ”™"], + "cool": ["๐Ÿ†’"], + "free": ["๐Ÿ†“"], + "abc": ["๐Ÿ”ค"], + "atm": ["๐Ÿง"], + "new": ["๐Ÿ†•"], + "sos": ["๐Ÿ†˜"], + "top": ["๐Ÿ”"], + "zzz": ["๐Ÿ’ค"], + "end": ["๐Ÿ”š"], + "ab": ["๐Ÿ†Ž"], + "cl": ["๐Ÿ†‘"], + "id": ["๐Ÿ†”"], + "ng": ["๐Ÿ†–"], + "no": ["โ™‘๏ธ"], + "ok": ["๐Ÿ†—"], + "on": ["๐Ÿ”›"], + "sy": ["๐Ÿ’ฑ"], + "tm": ["โ„ข๏ธ"], + "wc": ["๐Ÿšพ"], + "up": ["๐Ÿ†™"], + "a": ["๐Ÿ…ฐ๏ธ"], + "b": ["๐Ÿ…ฑ๏ธ"], + "c": ["โ˜ช๏ธ", "ยฉ", "๐Ÿฅ"], + "d": ["๐Ÿ‡ฉ"], + "e": ["๐Ÿ“ง", "๐Ÿ’ถ"], + "f": ["๐ŸŽ"], + "g": ["๐Ÿ‡ฌ"], + "h": ["๐Ÿจ", "๐Ÿฉ", "๐Ÿ‹โ€โ™€", "๐Ÿ‹โ€โ™‚"], + "i": ["โ„น๏ธ", "โ™Š๏ธ", "๐Ÿ••"], + "j": ["โคด๏ธ"], + "k": ["๐ŸŽ‹", "๐Ÿฆ…", "๐Ÿ’ƒ"], + "l": ["๐Ÿ›ด", "๐Ÿ•’"], + "m": ["โ™๏ธ", "โ“‚๏ธ", "ใ€ฝ๏ธ"], + "n": ["๐Ÿ“ˆ"], + "o": ["โญ•๏ธ", "๐Ÿ…พ๏ธ", "๐Ÿ“ฏ", "๐ŸŒ", "๐ŸŒš", "๐ŸŒ•", "๐Ÿฅฏ", "๐Ÿ™†โ€โ™€", "๐Ÿ™†โ€โ™‚"], + "p": ["๐Ÿ…ฟ๏ธ"], + "q": ["๐Ÿ”", "๐Ÿ€"], + "r": ["ยฎ"], + "s": ["๐Ÿ’ฐ", "๐Ÿ’ต", "๐Ÿ’ธ", "๐Ÿ’ฒ"], + "t": ["โœ๏ธ", "โฌ†๏ธ", "โ˜ฆ๏ธ"], + "u": ["โ›Ž", "โš“๏ธ", "๐Ÿ‰", "๐ŸŒ™", "๐Ÿ‹"], + "v": ["โœ…", "๐Ÿ”ฝ", "โ˜‘๏ธ", "โœ”๏ธ"], + "w": ["๐Ÿคทโ€โ™€", "๐Ÿคทโ€โ™‚", "๐Ÿคพโ€โ™€", "๐Ÿคพโ€โ™‚", "๐Ÿคฝโ€โ™€", "๐Ÿคฝโ€โ™‚"], + "x": ["๐Ÿ™…โ€โ™€", "๐Ÿ™…โ€โ™‚", "โŒ", "โŽ"], + "y": ["๐Ÿ’ด"], + "z": ["โšก๏ธ"] + } + + @classmethod + def _emojify(cls, string: str): + new_string = string.lower() + for key in cls._emojis: + selected_emoji = random.sample(cls._emojis[key], 1)[0] + new_string = new_string.replace(key, selected_emoji) + return new_string + + async def run(self, args: CommandArgs, data: CommandData) -> None: + string = args.joined(require_at_least=1) + await data.reply(self._emojify(string)) diff --git a/royalpack/commands/leagueoflegends.py b/royalpack/commands/leagueoflegends.py new file mode 100644 index 00000000..227afa53 --- /dev/null +++ b/royalpack/commands/leagueoflegends.py @@ -0,0 +1,224 @@ +import typing +import riotwatcher +import logging +import asyncio +import sentry_sdk +from royalnet.commands import * +from royalnet.utils import * +from ..tables import LeagueOfLegends +from ..utils import LeagueLeague + +log = logging.getLogger(__name__) + + +class LeagueoflegendsCommand(Command): + name: str = "leagueoflegends" + + aliases = ["lol", "league"] + + description: str = "Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche." + + syntax = "[nomeevocatore]" + + tables = {LeagueOfLegends} + + _region = "euw1" + + _telegram_group_id = -1001153723135 + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.interface.bot.get_secret("leagueoflegends")) + if self.interface.name == "telegram": + self.loop.create_task(self._updater(900)) + + async def _send(self, message): + client = self.interface.bot.client + await self.interface.bot.safe_api_call(client.send_message, + chat_id=self._telegram_group_id, + text=telegram_escape(message), + parse_mode="HTML", + disable_webpage_preview=True) + + async def _notify(self, + obj: LeagueOfLegends, + attribute_name: str, + old_value: typing.Any, + new_value: typing.Any): + if self.interface.name == "telegram": + if isinstance(old_value, LeagueLeague): + # This is a rank change! + # Don't send messages for every rank change, send messages just if the TIER or RANK changes! + if old_value.tier == new_value.tier and old_value.rank == new_value.rank: + return + # Find the queue + queue_names = { + "rank_soloq": "Solo/Duo", + "rank_flexq": "Flex", + "rank_twtrq": "3v3", + "rank_tftq": "TFT" + } + # Prepare the message + if new_value > old_value: + message = f"๐Ÿ“ˆ [b]{obj.user}[/b] รจ salito a {new_value} su League of Legends " \ + f"({queue_names[attribute_name]})! Congratulazioni!" + else: + message = f"๐Ÿ“‰ [b]{obj.user}[/b] รจ sceso a {new_value} su League of Legends " \ + f"({queue_names[attribute_name]})." + # Send the message + await self._send(message) + # Level up! + elif attribute_name == "summoner_level": + if new_value == 30 or (new_value >= 50 and (new_value % 25 == 0)): + await self._send(f"๐Ÿ†™ [b]{obj.user}[/b] รจ salito al livello [b]{new_value}[/b] su League of Legends!") + + @staticmethod + async def _change(obj: LeagueOfLegends, + attribute_name: str, + new_value: typing.Any, + callback: typing.Callable[ + [LeagueOfLegends, str, typing.Any, typing.Any], typing.Awaitable[None]]): + old_value = obj.__getattribute__(attribute_name) + if old_value != new_value: + await callback(obj, attribute_name, old_value, new_value) + obj.__setattr__(attribute_name, new_value) + + async def _update(self, lol: LeagueOfLegends): + log.info(f"Updating: {lol}") + log.debug(f"Getting summoner data: {lol}") + summoner = await asyncify(self._riotwatcher.summoner.by_id, region=self._region, + encrypted_summoner_id=lol.summoner_id) + await self._change(lol, "profile_icon_id", summoner["profileIconId"], self._notify) + await self._change(lol, "summoner_name", summoner["name"], self._notify) + await self._change(lol, "puuid", summoner["puuid"], self._notify) + await self._change(lol, "summoner_level", summoner["summonerLevel"], self._notify) + await self._change(lol, "summoner_id", summoner["id"], self._notify) + await self._change(lol, "account_id", summoner["accountId"], self._notify) + log.debug(f"Getting leagues data: {lol}") + leagues = await asyncify(self._riotwatcher.league.by_summoner, region=self._region, + encrypted_summoner_id=lol.summoner_id) + soloq = LeagueLeague() + flexq = LeagueLeague() + twtrq = LeagueLeague() + tftq = LeagueLeague() + for league in leagues: + if league["queueType"] == "RANKED_SOLO_5x5": + soloq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_SR": + flexq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_TT": + twtrq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_TFT": + tftq = LeagueLeague.from_dict(league) + await self._change(lol, "rank_soloq", soloq, self._notify) + await self._change(lol, "rank_flexq", flexq, self._notify) + await self._change(lol, "rank_twtrq", twtrq, self._notify) + await self._change(lol, "rank_tftq", tftq, self._notify) + log.debug(f"Getting mastery data: {lol}") + mastery = await asyncify(self._riotwatcher.champion_mastery.scores_by_summoner, region=self._region, + encrypted_summoner_id=lol.summoner_id) + await self._change(lol, "mastery_score", mastery, self._notify) + + async def _updater(self, period: int): + log.info(f"Started updater with {period}s period") + while True: + log.info(f"Updating...") + session = self.alchemy.Session() + log.info("") + lols = session.query(self.alchemy.LeagueOfLegends).all() + for lol in lols: + try: + await self._update(lol) + except Exception as e: + sentry_sdk.capture_exception(e) + log.error(f"Error while updating {lol.user.username}: {e}") + await asyncio.sleep(1) + await asyncify(session.commit) + session.close() + log.info(f"Sleeping for {period}s") + await asyncio.sleep(period) + + def _display(self, lol: LeagueOfLegends): + string = f"โ„น๏ธ [b]{lol.summoner_name}[/b]\n" \ + f"Lv. {lol.summoner_level}\n" \ + f"Mastery score: {lol.mastery_score}\n" \ + f"\n" + if lol.rank_soloq: + string += f"Solo: {lol.rank_soloq}\n" + if lol.rank_flexq: + string += f"Flex: {lol.rank_flexq}\n" + if lol.rank_twtrq: + string += f"3v3: {lol.rank_twtrq}\n" + if lol.rank_tftq: + string += f"TFT: {lol.rank_tftq}\n" + return string + + async def run(self, args: CommandArgs, data: CommandData) -> None: + author = await data.get_author(error_if_none=True) + + name = args.joined() + + if name: + # Connect a new League of Legends account to Royalnet + log.debug(f"Searching for: {name}") + summoner = self._riotwatcher.summoner.by_name(region=self._region, summoner_name=name) + # Ensure the account isn't already connected to something else + leagueoflegends = await asyncify( + data.session.query(self.alchemy.LeagueOfLegends).filter_by(summoner_id=summoner["id"]).one_or_none) + if leagueoflegends: + raise CommandError(f"L'account {leagueoflegends} รจ giร  registrato su Royalnet.") + # Get rank information + log.debug(f"Getting leagues data: {name}") + leagues = self._riotwatcher.league.by_summoner(region=self._region, encrypted_summoner_id=summoner["id"]) + soloq = LeagueLeague() + flexq = LeagueLeague() + twtrq = LeagueLeague() + tftq = LeagueLeague() + for league in leagues: + if league["queueType"] == "RANKED_SOLO_5x5": + soloq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_SR": + flexq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_TT": + twtrq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_TFT": + tftq = LeagueLeague.from_dict(league) + # Get mastery score + log.debug(f"Getting mastery data: {name}") + mastery = self._riotwatcher.champion_mastery.scores_by_summoner(region=self._region, + encrypted_summoner_id=summoner["id"]) + # Create database row + leagueoflegends = self.alchemy.LeagueOfLegends( + region=self._region, + user=author, + profile_icon_id=summoner["profileIconId"], + summoner_name=summoner["name"], + puuid=summoner["puuid"], + summoner_level=summoner["summonerLevel"], + summoner_id=summoner["id"], + account_id=summoner["accountId"], + rank_soloq=soloq, + rank_flexq=flexq, + rank_twtrq=twtrq, + rank_tftq=tftq, + mastery_score=mastery + ) + log.debug(f"Saving to the DB: {name}") + data.session.add(leagueoflegends) + await data.session_commit() + await data.reply(f"โ†”๏ธ Account {leagueoflegends} connesso a {author}!") + else: + # Update and display the League of Legends stats for the current account + if len(author.leagueoflegends) == 0: + raise CommandError("Nessun account di League of Legends trovato.") + message = "" + for account in author.leagueoflegends: + try: + await self._update(account) + message += self._display(account) + except riotwatcher.ApiError as e: + message += f"โš ๏ธ [b]{account.summoner_name}[/b]\n" \ + f"{e}" + message += "\n" + await data.session_commit() + await data.reply(message) diff --git a/royalpack/commands/matchmaking.py b/royalpack/commands/matchmaking.py new file mode 100644 index 00000000..3fe9c01c --- /dev/null +++ b/royalpack/commands/matchmaking.py @@ -0,0 +1,244 @@ +import datetime +import re +import dateparser +import typing +from telegram import Bot as PTBBot +from telegram import Message as PTBMessage +from telegram.error import BadRequest, Unauthorized +from telegram import InlineKeyboardMarkup as InKeMa +from telegram import InlineKeyboardButton as InKeBu +from royalnet.commands import * +from royalnet.bots import TelegramBot +from royalnet.utils import telegram_escape, asyncify, sleep_until +from ..tables import MMEvent, MMResponse, User +from ..utils import MMChoice, MMInterfaceDataTelegram + + +class MatchmakingCommand(Command): + name: str = "matchmaking" + + description: str = "Cerca persone per una partita a qualcosa!" + + syntax: str = "[ {ora} ] {nome}\n[descrizione]" + + aliases = ["mm", "lfg"] + + tables = {MMEvent, MMResponse} + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + # Find all relevant MMEvents and run them + session = self.alchemy.Session() + mmevents = ( + session + .query(self.alchemy.MMEvent) + .filter(self.alchemy.MMEvent.interface == self.interface.name, + self.alchemy.MMEvent.datetime > datetime.datetime.now()) + .all() + ) + for mmevent in mmevents: + self.interface.loop.create_task(self._run_mmevent(mmevent.mmid)) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + # Create a new MMEvent and run it + if self.interface.name != "telegram": + raise UnsupportedError(f"{self.interface.prefix}matchmaking funziona solo su Telegram. Per ora.") + author = await data.get_author(error_if_none=True) + + try: + timestring, title, description = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL) + except InvalidInputError: + timestring, title, description = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL) + try: + dt: typing.Optional[datetime.datetime] = dateparser.parse(timestring, settings={ + "PREFER_DATES_FROM": "future" + }) + except OverflowError: + dt = None + if dt is None: + raise CommandError("La data che hai specificato non รจ valida.") + if dt <= datetime.datetime.now(): + raise CommandError("La data che hai specificato รจ nel passato.") + if dt >= datetime.datetime.now() + datetime.timedelta(days=90): + raise CommandError("La data che hai specificato รจ a piรน di 90 giorni di distanza da oggi.") + mmevent: MMEvent = self.interface.alchemy.MMEvent(creator=author, + datetime=dt, + title=title, + description=description, + interface=self.interface.name) + data.session.add(mmevent) + await data.session_commit() + self.interface.loop.create_task(self._run_mmevent(mmevent.mmid)) + await data.reply(f"โœ… Evento [b]{mmevent.title}[/b] creato!") + + _mm_chat_id = -1001287169422 + + _mm_error_chat_id = -1001153723135 + + def _gen_mm_message(self, mmevent: MMEvent) -> str: + text = f"๐ŸŒ [{mmevent.datetime.strftime('%Y-%m-%d %H:%M')}] [b]{mmevent.title}[/b]\n" + if mmevent.description: + text += f"{mmevent.description}\n" + text += "\n" + for response in mmevent.responses: + response: MMResponse + text += f"{response.choice.value} {response.user}\n" + return text + + def _gen_telegram_keyboard(self, mmevent: MMEvent): + return InKeMa([ + [InKeBu(f"{MMChoice.YES.value} Ci sarรฒ!", callback_data=f"mm{mmevent.mmid}_YES")], + [InKeBu(f"{MMChoice.MAYBE.value} (Forse.)", callback_data=f"mm{mmevent.mmid}_MAYBE")], + [InKeBu(f"{MMChoice.LATE_SHORT.value} Arrivo dopo 5-10 min.", + callback_data=f"mm{mmevent.mmid}_LATE_SHORT")], + [InKeBu(f"{MMChoice.LATE_MEDIUM.value} Arrivo dopo 15-35 min.", + callback_data=f"mm{mmevent.mmid}_LATE_MEDIUM")], + [InKeBu(f"{MMChoice.LATE_LONG.value} Arrivo dopo 40+ min.", callback_data=f"mm{mmevent.mmid}_LATE_LONG")], + [InKeBu(f"{MMChoice.NO_TIME.value} Non posso a quell'ora...", callback_data=f"mm{mmevent.mmid}_NO_TIME")], + [InKeBu(f"{MMChoice.NO_INTEREST.value} Non mi interessa.", callback_data=f"mm{mmevent.mmid}_NO_INTEREST")], + [InKeBu(f"{MMChoice.NO_TECH.value} Ho un problema!", callback_data=f"mm{mmevent.mmid}_NO_TECH")], + ]) + + async def _update_telegram_mm_message(self, client: PTBBot, mmevent: MMEvent): + try: + await self.interface.bot.safe_api_call(client.edit_message_text, + chat_id=self._mm_chat_id, + text=telegram_escape(self._gen_mm_message(mmevent)), + message_id=mmevent.interface_data.message_id, + parse_mode="HTML", + disable_web_page_preview=True, + reply_markup=self._gen_telegram_keyboard(mmevent)) + except BadRequest: + pass + + def _gen_mm_telegram_callback(self, client: PTBBot, mmid: int, choice: MMChoice): + async def callback(data: CommandData): + author = await data.get_author(error_if_none=True) + # Find the MMEvent with the current session + mmevent: MMEvent = await asyncify(data.session.query(self.alchemy.MMEvent).get, mmid) + mmresponse: MMResponse = await asyncify( + data.session.query(self.alchemy.MMResponse).filter_by(user=author, mmevent=mmevent).one_or_none) + if mmresponse is None: + mmresponse = self.alchemy.MMResponse(user=author, mmevent=mmevent, choice=choice) + data.session.add(mmresponse) + else: + mmresponse.choice = choice + await data.session_commit() + await self._update_telegram_mm_message(client, mmevent) + return f"โœ… Messaggio ricevuto!" + + return callback + + def _gen_event_start_message(self, mmevent: MMEvent): + text = f"๐Ÿšฉ L'evento [b]{mmevent.title}[/b] รจ iniziato!\n\n" + for response in mmevent.responses: + response: MMResponse + text += f"{response.choice.value} {response.user}\n" + return text + + def _gen_unauth_message(self, user: User): + return f"โš ๏ธ Non sono autorizzato a mandare messaggi a [b]{user.username}[/b]!\n" \ + f"{user.telegram.mention()}, apri una chat privata con me e mandami un messaggio!" + + async def _run_mmevent(self, mmid: int): + """Run a MMEvent.""" + # Open a new Alchemy Session + session = self.alchemy.Session() + # Find the MMEvent with the current session + mmevent: MMEvent = await asyncify(session.query(self.alchemy.MMEvent).get, mmid) + if mmevent is None: + raise ValueError("Invalid mmid.") + # Ensure the MMEvent hasn't already started + if mmevent.datetime <= datetime.datetime.now(): + raise ValueError("MMEvent has already started.") + # Ensure the MMEvent interface matches the current one + if mmevent.interface != self.interface.name: + raise ValueError("Invalid interface.") + # If the matchmaking message hasn't been sent yet, do so now + if mmevent.interface_data is None: + if self.interface.name == "telegram": + bot: TelegramBot = self.interface.bot + client: PTBBot = bot.client + # Build the Telegram keyboard + # Send the keyboard + message: PTBMessage = await self.interface.bot.safe_api_call(client.send_message, + chat_id=self._mm_chat_id, + text=telegram_escape( + self._gen_mm_message(mmevent)), + parse_mode="HTML", + disable_webpage_preview=True, + reply_markup=self._gen_telegram_keyboard( + mmevent)) + # Store message data in the interface data object + mmevent.interface_data = MMInterfaceDataTelegram(chat_id=self._mm_chat_id, + message_id=message.message_id) + await asyncify(session.commit) + else: + raise UnsupportedError() + # Register handlers for the keyboard events + if self.interface.name == "telegram": + bot: TelegramBot = self.interface.bot + client: PTBBot = bot.client + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_YES", + callback=self._gen_mm_telegram_callback(client, mmid, MMChoice.YES)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_MAYBE", + callback=self._gen_mm_telegram_callback(client, mmid, MMChoice.MAYBE)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_SHORT", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.LATE_SHORT)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_MEDIUM", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.LATE_MEDIUM)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_LONG", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.LATE_LONG)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_TIME", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.NO_TIME)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_INTEREST", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.NO_INTEREST)) + self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_TECH", + callback=self._gen_mm_telegram_callback(client, mmid, + MMChoice.NO_TECH)) + else: + raise UnsupportedError() + # Sleep until the time of the event + await sleep_until(mmevent.datetime) + # Notify the positive answers of the event start + if self.interface.name == "telegram": + bot: TelegramBot = self.interface.bot + client: PTBBot = bot.client + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_YES") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_MAYBE") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_SHORT") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_MEDIUM") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_LONG") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_TIME") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_INTEREST") + self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_TECH") + for response in mmevent.responses: + if response.choice == MMChoice.NO_INTEREST or response.choice == MMChoice.NO_TIME: + return + try: + await self.interface.bot.safe_api_call(client.send_message, + chat_id=response.user.telegram[0].tg_id, + text=telegram_escape(self._gen_event_start_message(mmevent)), + parse_mode="HTML", + disable_webpage_preview=True) + except Unauthorized: + await self.interface.bot.safe_api_call(client.send_message, + chat_id=self._mm_error_chat_id, + text=telegram_escape( + self._gen_unauth_message(response.user)), + parse_mode="HTML", + disable_webpage_preview=True) + else: + raise UnsupportedError() + # Delete the event message + if self.interface.name == "telegram": + await self.interface.bot.safe_api_call(client.delete_message, + chat_id=mmevent.interface_data.chat_id, + message_id=mmevent.interface_data.message_id) + # The end! + await asyncify(session.close) diff --git a/royalpack/commands/mp3.py b/royalpack/commands/mp3.py new file mode 100644 index 00000000..45efec4c --- /dev/null +++ b/royalpack/commands/mp3.py @@ -0,0 +1,40 @@ +import typing +import urllib.parse +import asyncio +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.audio import YtdlMp3 + + +class Mp3Command(Command): + name: str = "mp3" + + aliases = ["dlmusic"] + + description: str = "Scarica un video con youtube-dl e invialo in chat." + + syntax = "{ytdlstring}" + + ytdl_args = { + "format": "bestaudio", + "outtmpl": f"./downloads/%(title)s.%(ext)s" + } + + seconds_before_deletion = 15 * 60 + + async def run(self, args: CommandArgs, data: CommandData) -> None: + url = args.joined() + if url.startswith("http://") or url.startswith("https://"): + vfiles: typing.List[YtdlMp3] = await asyncify(YtdlMp3.create_and_ready_from_url, + url, + **self.ytdl_args) + else: + vfiles = await asyncify(YtdlMp3.create_and_ready_from_url, f"ytsearch:{url}", **self.ytdl_args) + for vfile in vfiles: + await data.reply(f"โฌ‡๏ธ Il file richiesto puรฒ essere scaricato a:\n" + f"https://scaleway.steffo.eu/{urllib.parse.quote(vfile.mp3_filename.replace('./downloads/', './musicbot_cache/'))}\n" + f"Verrร  eliminato tra {self.seconds_before_deletion} secondi.") + await asyncio.sleep(self.seconds_before_deletion) + for vfile in vfiles: + vfile.delete() + await data.reply(f"โน Il file {vfile.info.title} รจ scaduto ed รจ stato eliminato.") diff --git a/royalpack/commands/pause.py b/royalpack/commands/pause.py new file mode 100644 index 00000000..14147e0e --- /dev/null +++ b/royalpack/commands/pause.py @@ -0,0 +1,53 @@ +import typing +import discord +from royalnet.commands import * +from royalnet.bots import DiscordBot + + +class PauseCommand(Command): + name: str = "pause" + + description: str = "Mette in pausa o riprende la riproduzione della canzone attuale." + + syntax = "[ [guild] ]" + + @staticmethod + async def _legacy_pause_handler(bot: DiscordBot, guild_name: typing.Optional[str]): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("No guilds with the specified name found.") + if len(guilds) > 1: + raise CommandError("Multiple guilds with the specified name found.") + guild = list(bot.client.guilds)[0] + # Set the currently playing source as ended + voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild) + if not (voice_client.is_playing() or voice_client.is_paused()): + raise CommandError("There is nothing to pause.") + # Toggle pause + resume = voice_client._player.is_paused() + if resume: + voice_client._player.resume() + else: + voice_client._player.pause() + return {"resumed": resume} + + _event_name = "_legacy_pause" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_pause_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, = args.match(r"(?:\[(.+)])?") + response = await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name + }) + if response["resumed"]: + await data.reply(f"โ–ถ๏ธ Riproduzione ripresa.") + else: + await data.reply(f"โธ Riproduzione messa in pausa.") diff --git a/royalpack/commands/peertube.py b/royalpack/commands/peertube.py new file mode 100644 index 00000000..f0c60a45 --- /dev/null +++ b/royalpack/commands/peertube.py @@ -0,0 +1,81 @@ +import aiohttp +import asyncio +import datetime +import logging +import dateparser +from royalnet.commands import * +from royalnet.utils import telegram_escape + + +log = logging.getLogger(__name__) + + +class PeertubeCommand(Command): + name: str = "peertube" + + description: str = "Guarda quando รจ uscito l'ultimo video su RoyalTube." + + _url = r"https://pt.steffo.eu/feeds/videos.json?sort=-publishedAt&filter=local" + + _ready = asyncio.Event() + + _timeout = 300 + + _latest_date: datetime.datetime = None + + _telegram_group_id = -1001153723135 + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if self.interface.name == "telegram": + self.loop.create_task(self._ready_up()) + self.loop.create_task(self._update()) + + async def _get_json(self): + log.debug("Getting jsonfeed") + async with aiohttp.ClientSession() as session: + async with session.get(self._url) as response: + log.debug("Parsing jsonfeed") + j = await response.json() + log.debug("Jsonfeed parsed successfully") + return j + + async def _send(self, message): + client = self.interface.bot.client + await self.interface.bot.safe_api_call(client.send_message, + chat_id=self._telegram_group_id, + text=telegram_escape(message), + parse_mode="HTML", + disable_webpage_preview=True) + + async def _ready_up(self): + j = await self._get_json() + if j["version"] != "https://jsonfeed.org/version/1": + raise ConfigurationError("_url is not a jsonfeed") + videos = j["items"] + for video in reversed(videos): + date_modified = dateparser.parse(video["date_modified"]) + if self._latest_date is None or date_modified > self._latest_date: + log.debug(f"Found newer video: {date_modified}") + self._latest_date = date_modified + self._ready.set() + + async def _update(self): + await self._ready.wait() + while True: + j = await self._get_json() + videos = j["items"] + for video in reversed(videos): + date_modified = dateparser.parse(video["date_modified"]) + if date_modified > self._latest_date: + log.debug(f"Found newer video: {date_modified}") + self._latest_date = date_modified + await self._send(f"๐Ÿ†• Nuovo video su RoyalTube!\n" + f"[b]{video['title']}[/b]\n" + f"{video['url']}") + await asyncio.sleep(self._timeout) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name != "telegram": + raise UnsupportedError() + await data.reply(f"โ„น๏ธ Ultimo video caricato il: [b]{self._latest_date.isoformat()}[/b]") diff --git a/royalpack/commands/play.py b/royalpack/commands/play.py new file mode 100644 index 00000000..44e7823d --- /dev/null +++ b/royalpack/commands/play.py @@ -0,0 +1,79 @@ +import typing +import pickle +import datetime +import discord +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.audio import YtdlDiscord +from royalnet.bots import DiscordBot + + +class PlayCommand(Command): + name: str = "play" + + aliases = ["p"] + + description: str = "Aggiunge un url alla coda della chat vocale." + + syntax = "[ [guild] ] {url}" + + @staticmethod + async def _legacy_play_handler(bot: "DiscordBot", guild_name: typing.Optional[str], url: str): + """Handle a play Royalnet request. That is, add audio to a PlayMode.""" + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("Server non trovato.") + if len(guilds) > 1: + raise CommandError("Il nome del server รจ ambiguo.") + guild = list(bot.client.guilds)[0] + # Ensure the guild has a PlayMode before adding the file to it + if not bot.music_data.get(guild): + raise CommandError("Il bot non รจ in nessun canale vocale.") + # Create url + ytdl_args = { + "format": "bestaudio/best", + "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s" + } + # Start downloading + dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, url, **ytdl_args) + await bot.add_to_music_data(dfiles, guild) + # Create response dictionary + response = { + "videos": [{ + "title": dfile.info.title, + "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed())) + } for dfile in dfiles] + } + return response + + _event_name = "_legacy_play" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_play_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, url = args.match(r"(?:\[(.+)])?\s*?") + if not (url.startswith("http://") or url.startswith("https://")): + raise CommandError(f"Il comando [c]{self.interface.prefix}play[/c] funziona solo per riprodurre file da" + f" un URL.\n" + f"Se vuoi cercare un video, usa [c]{self.interface.prefix}youtube[/c] o" + f" [c]{self.interface.prefix}soundcloud[/c]!") + response: dict = await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "url": url + }) + if len(response["videos"]) == 0: + raise CommandError(f"Nessun file trovato.") + for video in response["videos"]: + if self.interface.name == "discord": + # This is one of the unsafest things ever + embed = pickle.loads(eval(video["discord_embed_pickle"])) + await data.message.channel.send(content="โ–ถ๏ธ Aggiunto alla coda:", embed=embed) + else: + await data.reply(f"โ–ถ๏ธ Aggiunto alla coda: [i]{video['title']}[/i]") diff --git a/royalpack/commands/playmode.py b/royalpack/commands/playmode.py new file mode 100644 index 00000000..f99a5616 --- /dev/null +++ b/royalpack/commands/playmode.py @@ -0,0 +1,57 @@ +import typing +import discord +from royalnet.commands import * +from royalnet.audio.playmodes import Playlist, Pool, Layers +from royalnet.bots import DiscordBot + + +class PlaymodeCommand(Command): + name: str = "playmode" + + aliases = ["pm", "mode"] + + description: str = "Cambia modalitร  di riproduzione per la chat vocale." + + syntax = "[ [guild] ] {mode}" + + @staticmethod + async def _legacy_playmode_handler(bot: "DiscordBot", guild_name: typing.Optional[str], mode_name: str): + """Handle a playmode Royalnet request. That is, change current PlayMode.""" + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("No guilds with the specified name found.") + if len(guilds) > 1: + raise CommandError("Multiple guilds with the specified name found.") + guild = list(bot.client.guilds)[0] + # Delete the previous PlayMode, if it exists + if bot.music_data[guild] is not None: + bot.music_data[guild].playmode.delete() + # Create the new PlayMode + if mode_name == "playlist": + bot.music_data[guild].playmode = Playlist() + elif mode_name == "pool": + bot.music_data[guild].playmode = Pool() + elif mode_name == "layers": + bot.music_data[guild].playmode = Layers() + else: + raise CommandError("Unknown PlayMode specified.") + return {} + + _event_name = "_legacy_playmode" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_playmode_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, mode_name = args.match(r"(?:\[(.+)])?\s*(\S+)\s*") + await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "mode_name": mode_name + }) + await data.reply(f"๐Ÿ”ƒ Impostata la modalitร  di riproduzione a: [c]{mode_name}[/c].") diff --git a/royalpack/commands/queue.py b/royalpack/commands/queue.py new file mode 100644 index 00000000..a242d83f --- /dev/null +++ b/royalpack/commands/queue.py @@ -0,0 +1,93 @@ +import typing +import pickle +import discord +from royalnet.commands import * +from royalnet.utils import numberemojiformat +from royalnet.bots import DiscordBot + + +class QueueCommand(Command): + name: str = "queue" + + aliases = ["q"] + + description: str = "Visualizza la coda di riproduzione attuale." + + syntax = "[ [guild] ]" + + @staticmethod + async def _legacy_queue_handler(bot: "DiscordBot", guild_name: typing.Optional[str]): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("No guilds with the specified name found.") + if len(guilds) > 1: + raise CommandError("Multiple guilds with the specified name found.") + guild = list(bot.client.guilds)[0] + # Check if the guild has a PlayMode + playmode = bot.music_data.get(guild) + if not playmode: + return { + "type": None + } + try: + queue = playmode.queue_preview() + except NotImplementedError: + return { + "type": playmode.__class__.__name__ + } + return { + "type": playmode.__class__.__name__, + "queue": + { + "strings": [str(dfile.info) for dfile in queue], + "pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue])) + } + } + + _event_name = "_legacy_queue" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_queue_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, = args.match(r"(?:\[(.+)])?") + response = await self.interface.call_herald_action("discord", self._event_name, {"guild_name": guild_name}) + if response["type"] is None: + await data.reply("โ„น๏ธ Non c'รจ nessuna coda di riproduzione attiva al momento.") + return + elif "queue" not in response: + await data.reply(f"โ„น๏ธ La coda di riproduzione attuale ([c]{response['type']}[/c]) non permette l'anteprima.") + return + if response["type"] == "Playlist": + if len(response["queue"]["strings"]) == 0: + message = f"โ„น๏ธ Questa [c]Playlist[/c] รจ vuota." + else: + message = f"โ„น๏ธ Questa [c]Playlist[/c] contiene {len(response['queue']['strings'])} elementi, e i prossimi saranno:\n" + elif response["type"] == "Pool": + if len(response["queue"]["strings"]) == 0: + message = f"โ„น๏ธ Questo [c]Pool[/c] รจ vuoto." + else: + message = f"โ„น๏ธ Questo [c]Pool[/c] contiene {len(response['queue']['strings'])} elementi, tra cui:\n" + elif response["type"] == "Layers": + if len(response["queue"]["strings"]) == 0: + message = f"โ„น๏ธ Nessun elemento รจ attualmente in riproduzione, pertanto non ci sono [c]Layers[/c]:" + else: + message = f"โ„น๏ธ I [c]Layers[/c] dell'elemento attualmente in riproduzione sono {len(response['queue']['strings'])}, tra cui:\n" + else: + if len(response["queue"]["strings"]) == 0: + message = f"โ„น๏ธ Il PlayMode attuale, [c]{response['type']}[/c], รจ vuoto.\n" + else: + message = f"โ„น๏ธ Il PlayMode attuale, [c]{response['type']}[/c], contiene {len(response['queue']['strings'])} elementi:\n" + if self.interface.name == "discord": + await data.reply(message) + for embed in pickle.loads(eval(response["queue"]["pickled_embeds"]))[:5]: + await data.message.channel.send(embed=embed) + else: + message += numberemojiformat(response["queue"]["strings"][:10]) + await data.reply(message) diff --git a/royalpack/commands/rage.py b/royalpack/commands/rage.py new file mode 100644 index 00000000..91435811 --- /dev/null +++ b/royalpack/commands/rage.py @@ -0,0 +1,20 @@ +import typing +import random +from royalnet.commands import * + + +class RageCommand(Command): + name: str = "rage" + + aliases = ["balurage", "madden"] + + description: str = "Arrabbiati per qualcosa, come una software house californiana." + + MAD = ["MADDEN MADDEN MADDEN MADDEN", + "EA bad, praise Geraldo!", + "Stai sfogando la tua ira sul bot!", + "Basta, io cambio gilda!", + "Fondiamo la RRYG!"] + + async def run(self, args: CommandArgs, data: CommandData) -> None: + await data.reply(f"๐Ÿ˜  {random.sample(self.MAD, 1)[0]}") diff --git a/royalpack/commands/reminder.py b/royalpack/commands/reminder.py new file mode 100644 index 00000000..e33b5e80 --- /dev/null +++ b/royalpack/commands/reminder.py @@ -0,0 +1,86 @@ +import typing +import dateparser +import datetime +import pickle +import telegram +import discord +from sqlalchemy import and_ +from royalnet.commands import * +from royalnet.utils import sleep_until, asyncify, telegram_escape, discord_escape +from ..tables import Reminder + + +class ReminderCommand(Command): + name: str = "reminder" + + aliases = ["calendar"] + + description: str = "Ti ricorda di fare qualcosa dopo un po' di tempo." + + syntax: str = "[ {data} ] {messaggio}" + + tables = {Reminder} + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + session = interface.alchemy.Session() + reminders = ( + session.query(interface.alchemy.Reminder) + .filter(and_( + interface.alchemy.Reminder.datetime >= datetime.datetime.now(), + interface.alchemy.Reminder.interface_name == interface.name)) + .all() + ) + for reminder in reminders: + interface.loop.create_task(self._remind(reminder)) + + async def _remind(self, reminder): + await sleep_until(reminder.datetime) + if self.interface.name == "telegram": + chat_id: int = pickle.loads(reminder.raw_interface_data) + bot: telegram.Bot = self.interface.bot.client + await asyncify(bot.send_message, + chat_id=chat_id, + text=telegram_escape(f"โ—๏ธ {reminder.message}"), + parse_mode="HTML", + disable_web_page_preview=True) + elif self.interface.name == "discord": + channel_id: int = pickle.loads(reminder.raw_interface_data) + bot: discord.Client = self.interface.bot.client + channel = bot.get_channel(channel_id) + await channel.send(discord_escape(f"โ—๏ธ {reminder.message}")) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + try: + date_str, reminder_text = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*") + except InvalidInputError: + date_str, reminder_text = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*") + + try: + date: typing.Optional[datetime.datetime] = dateparser.parse(date_str, settings={ + "PREFER_DATES_FROM": "future" + }) + except OverflowError: + date = None + if date is None: + await data.reply("โš ๏ธ La data che hai inserito non รจ valida.") + return + if date <= datetime.datetime.now(): + await data.reply("โš ๏ธ La data che hai specificato รจ nel passato.") + return + await data.reply(f"โœ… Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]") + if self.interface.name == "telegram": + interface_data = pickle.dumps(data.update.effective_chat.id) + elif self.interface.name == "discord": + interface_data = pickle.dumps(data.message.channel.id) + else: + raise UnsupportedError("This command does not support the current interface.") + creator = await data.get_author() + reminder = self.interface.alchemy.Reminder(creator=creator, + interface_name=self.interface.name, + interface_data=interface_data, + datetime=date, + message=reminder_text) + self.interface.loop.create_task(self._remind(reminder)) + data.session.add(reminder) + await asyncify(data.session.commit) diff --git a/royalpack/commands/ship.py b/royalpack/commands/ship.py new file mode 100644 index 00000000..83677766 --- /dev/null +++ b/royalpack/commands/ship.py @@ -0,0 +1,40 @@ +import typing +import re +from royalnet.commands import * +from royalnet.utils import safeformat + + +class ShipCommand(Command): + name: str = "ship" + + aliases = ["โ›ต๏ธ"] + + description: str = "Crea una ship tra due nomi." + + syntax = "{nomeuno} {nomedue}" + + async def run(self, args: CommandArgs, data: CommandData) -> None: + name_one = args[0] + name_two = args[1] + if name_two == "+": + name_two = args[2] + name_one = name_one.lower() + name_two = name_two.lower() + # Get all letters until the first vowel, included + match_one = re.search(r"^[A-Za-z][^aeiouAEIOU]*[aeiouAEIOU]?", name_one) + if match_one is None: + part_one = name_one[:int(len(name_one) / 2)] + else: + part_one = match_one.group(0) + # Get all letters from the second to last vowel, excluded + match_two = re.search(r"[^aeiouAEIOU]*[aeiouAEIOU]?[A-Za-z]$", name_two) + if match_two is None: + part_two = name_two[int(len(name_two) / 2):] + else: + part_two = match_two.group(0) + # Combine the two name parts + mixed = part_one + part_two + await data.reply(safeformat("๐Ÿ’• {one} + {two} = [b]{result}[/b]", + one=name_one.capitalize(), + two=name_two.capitalize(), + result=mixed.capitalize())) diff --git a/royalpack/commands/skip.py b/royalpack/commands/skip.py new file mode 100644 index 00000000..48e2a4ed --- /dev/null +++ b/royalpack/commands/skip.py @@ -0,0 +1,48 @@ +import typing +import discord +from royalnet.commands import * +from royalnet.bots import DiscordBot + + +class SkipCommand(Command): + name: str = "skip" + + aliases = ["s", "next", "n"] + + description: str = "Salta la canzone attualmente in riproduzione in chat vocale." + + syntax: str = "[ [guild] ]" + + @staticmethod + async def _legacy_skip_handler(bot: "DiscordBot", guild_name: typing.Optional[str]): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("No guilds with the specified name found.") + if len(guilds) > 1: + raise CommandError("Multiple guilds with the specified name found.") + guild = list(bot.client.guilds)[0] + # Set the currently playing source as ended + voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild) + if voice_client and not (voice_client.is_playing() or voice_client.is_paused()): + raise CommandError("Nothing to skip") + # noinspection PyProtectedMember + voice_client._player.stop() + return {} + + _event_name = "_legacy_skip" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_skip_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, = args.match(r"(?:\[(.+)])?") + await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name + }) + await data.reply(f"โฉ Richiesto lo skip della canzone attuale.") diff --git a/royalpack/commands/smecds.py b/royalpack/commands/smecds.py new file mode 100644 index 00000000..d83f453a --- /dev/null +++ b/royalpack/commands/smecds.py @@ -0,0 +1,70 @@ +import typing +import random +from royalnet.commands import * +from royalnet.utils import safeformat + + +class SmecdsCommand(Command): + name: str = "smecds" + + aliases = ["secondomeecolpadellostagista"] + + description: str = "Secondo me, รจ colpa dello stagista..." + + syntax = "" + + DS_LIST = ["della secca", "del seccatore", "del secchiello", "del secchio", "del secchione", "del secondino", + "del sedano", "del sedativo", "della sedia", "del sedicente", "del sedile", "della sega", "del segale", + "della segatura", "della seggiola", "del seggiolino", "della seggiovia", "della segheria", + "del seghetto", "del segnalibro", "del segnaposto", "del segno", "del segretario", "della segreteria", + "del seguace", "del segugio", "della selce", "della sella", "della selz", "della selva", + "della selvaggina", "del semaforo", "del seme", "del semifreddo", "del seminario", "della seminarista", + "della semola", "del semolino", "del semplicione", "della senape", "del senatore", "del seno", + "del sensore", "della sentenza", "della sentinella", "del sentore", "della seppia", "del sequestratore", + "della serenata", "del sergente", "del sermone", "della serpe", "del serpente", "della serpentina", + "della serra", "del serraglio", "del serramanico", "della serranda", "della serratura", "del servitore", + "della servitรน", "del servizievole", "del servo", "del set", "della seta", "della setola", "del sigaro", + "del sidecar", "del siderurgico", "del sidro", "della siepe", "del sifone", "della sigaretta", + "del sigillo", "della signora", "della signorina", "del silenziatore", "della silhouette", "del silicio", + "del silicone", "del siluro", "della sinagoga", "della sindacalista", "del sindacato", "del sindaco", + "della sindrome", "della sinfonia", "del sipario", "del sire", "della sirena", "della siringa", + "del sismografo", "del sobborgo", "del sobillatore", "del sobrio", "del soccorritore", "del socio", + "del sociologo", "della soda", "del sofร ", "della soffitta", "del software", "dello sogghignare", + "del soggiorno", "della sogliola", "del sognatore", "della soia", "del solaio", "del solco", + "del soldato", "del soldo", "del sole", "della soletta", "della solista", "del solitario", + "del sollazzare", "del sollazzo", "del sollecito", "del solleone", "del solletico", "del sollevare", + "del sollievo", "del solstizio", "del solubile", "del solvente", "della soluzione", "del somaro", + "del sombrero", "del sommergibile", "del sommo", "della sommossa", "del sommozzatore", "del sonar", + "della sonda", "del sondaggio", "del sondare", "del sonnacchioso", "del sonnambulo", "del sonnellino", + "del sonnifero", "del sonno", "della sonnolenza", "del sontuoso", "del soppalco", "del soprabito", + "del sopracciglio", "del sopraffare", "del sopraffino", "del sopraluogo", "del sopramobile", + "del soprannome", "del soprano", "del soprappensiero", "del soprassalto", "del soprassedere", + "del sopravvento", "del sopravvivere", "del soqquadro", "del sorbetto", "del sordido", "della sordina", + "del sordo", "della sorella", "della sorgente", "del sornione", "del sorpasso", "della sorpresa", + "del sorreggere", "del sorridere", "della sorsata", "del sorteggio", "del sortilegio", + "del sorvegliante", "del sorvolare", "del sosia", "del sospettoso", "del sospirare", "della sosta", + "della sostanza", "del sostegno", "del sostenitore", "del sostituto", "del sottaceto", "della sottana", + "del sotterfugio", "del sotterraneo", "del sottile", "del sottilizzare", "del sottintendere", + "del sottobanco", "del sottobosco", "del sottomarino", "del sottopassaggio", "del sottoposto", + "del sottoscala", "della sottoscrizione", "del sottostare", "del sottosuolo", "del sottotetto", + "del sottotitolo", "del sottovalutare", "del sottovaso", "della sottoveste", "del sottovuoto", + "del sottufficiale", "della soubrette", "del souvenir", "del soverchiare", "del sovrano", + "del sovrapprezzo", "della sovvenzione", "del sovversivo", "del sozzo", "dello suadente", "del sub", + "del subalterno", "del subbuglio", "del subdolo", "del sublime", "del suburbano", "del successore", + "del succo", "della succube", "del succulento", "della succursale", "del sudario", "della sudditanza", + "del suddito", "del sudicio", "del suffisso", "del suffragio", "del suffumigio", "del suggeritore", + "del sughero", "del sugo", "del suino", "della suite", "del sulfureo", "del sultano", "di Steffo", + "di Spaggia", "di Sabrina", "del sas", "del ses", "del sis", "del sos", "del sus", "della supremazia", + "del Santissimo", "della scatola", "del supercalifragilistichespiralidoso", "del sale", "del salame", + "di (Town of) Salem", "di Stronghold", "di SOMA", "dei Saints", "di S.T.A.L.K.E.R.", "di Sanctum", + "dei Sims", "di Sid", "delle Skullgirls", "di Sonic", "di Spiral (Knights)", "di Spore", "di Starbound", + "di SimCity", "di Sensei", "di Ssssssssssssss... Boom! E' esploso il dizionario", "della scala", + "di Sakura", "di Suzie", "di Shinji", "del senpai", "del support", "di Superman", "di Sekiro", + "dello Slime God", "del salassato", "della salsa", "di Senjougahara", "di Sugar", "della Stampa", + "della Stampante"] + + SMECDS = "๐Ÿค” Secondo me, รจ colpa {ds}." + + async def run(self, args: CommandArgs, data: CommandData) -> None: + ds = random.sample(self.DS_LIST, 1)[0] + await data.reply(safeformat(self.SMECDS, ds=ds)) diff --git a/royalpack/commands/soundcloud.py b/royalpack/commands/soundcloud.py new file mode 100644 index 00000000..64ad73d2 --- /dev/null +++ b/royalpack/commands/soundcloud.py @@ -0,0 +1,77 @@ +import typing +import pickle +import datetime +import discord +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.audio import YtdlDiscord +from royalnet.bots import DiscordBot + + +class SoundcloudCommand(Command): + name: str = "soundcloud" + + aliases = ["sc"] + + description: str = "Cerca una canzone su Soundcloud e la aggiunge alla coda della chat vocale." + + syntax = "[ [guild] ] {url}" + + @staticmethod + async def _legacy_soundcloud_handler(bot: "DiscordBot", guild_name: typing.Optional[str], search: str): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("Server non trovato.") + if len(guilds) > 1: + raise CommandError("Il nome del server รจ ambiguo.") + guild = list(bot.client.guilds)[0] + # Ensure the guild has a PlayMode before adding the file to it + if not bot.music_data.get(guild): + raise CommandError("Il bot non รจ in nessun canale vocale.") + # Create url + ytdl_args = { + "format": "bestaudio/best", + "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s" + } + # Start downloading + dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'scsearch:{search}', + **ytdl_args) + await bot.add_to_music_data(dfiles, guild) + # Create response dictionary + return { + "videos": [{ + "title": dfile.info.title, + "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed())) + } for dfile in dfiles] + } + + _event_name = "_legacy_soundcloud" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_soundcloud_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, search = args.match(r"(?:\[(.+)])?\s*?") + if search.startswith("http://") or search.startswith("https://"): + raise CommandError(f"Il comando [c]{self.interface.prefix}soundcloud[/c] funziona solo per cercare audio su" + f" Soundcloud con un dato nome.\n" + f"Se vuoi riprodurre una canzone da un URL, usa [c]{self.interface.prefix}play[/c]!") + response = await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "search": search + }) + if len(response["videos"]) == 0: + raise CommandError(f"Il video non puรฒ essere scaricato a causa di un blocco imposto da Soundcloud.") + for video in response["videos"]: + if self.interface.name == "discord": + # This is one of the unsafest things ever + embed = pickle.loads(eval(video["discord_embed_pickle"])) + await data.message.channel.send(content="โ–ถ๏ธ Aggiunto alla coda:", embed=embed) + else: + await data.reply(f"โ–ถ๏ธ Aggiunto alla coda: [i]{video['title']}[/i]") diff --git a/royalpack/commands/summon.py b/royalpack/commands/summon.py new file mode 100644 index 00000000..c38c9edd --- /dev/null +++ b/royalpack/commands/summon.py @@ -0,0 +1,77 @@ +import typing +import discord +from royalnet.commands import * +from royalnet.bots import DiscordBot + + +class SummonCommand(Command): + name: str = "summon" + + aliases = ["cv"] + + description: str = "Evoca il bot in un canale vocale." + + syntax: str = "[nomecanale]" + + @staticmethod + async def _legacy_summon_handler(bot: "DiscordBot", channel_name: str): + """Handle a summon Royalnet request. + That is, join a voice channel, or move to a different one if that is not possible.""" + channels = bot.client.find_channel_by_name(channel_name) + if len(channels) < 1: + raise CommandError(f"Nessun canale vocale con il nome [c]{channel_name}[/c] trovato.") + channel = channels[0] + if not isinstance(channel, discord.VoiceChannel): + raise CommandError(f"Il canale [c]{channel}[/c] non รจ un canale vocale.") + bot.loop.create_task(bot.client.vc_connect_or_move(channel)) + return {} + + _event_name = "_legacy_summon" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_summon_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name == "discord": + bot = self.interface.bot.client + message: discord.Message = data.message + channel_name: str = args.optional(0) + if channel_name: + guild: typing.Optional[discord.Guild] = message.guild + if guild is not None: + channels: typing.List[discord.abc.GuildChannel] = guild.channels + else: + channels = bot.get_all_channels() + matching_channels: typing.List[discord.VoiceChannel] = [] + for channel in channels: + if isinstance(channel, discord.VoiceChannel): + if channel.name == channel_name: + matching_channels.append(channel) + if len(matching_channels) == 0: + await data.reply("โš ๏ธ Non esiste alcun canale vocale con il nome specificato.") + return + elif len(matching_channels) > 1: + await data.reply("โš ๏ธ Esiste piรน di un canale vocale con il nome specificato.") + return + channel = matching_channels[0] + else: + author: discord.Member = message.author + try: + voice: typing.Optional[discord.VoiceState] = author.voice + except AttributeError: + await data.reply("โš ๏ธ Non puoi evocare il bot da una chat privata!") + return + if voice is None: + await data.reply("โš ๏ธ Non sei connesso a nessun canale vocale!") + return + channel = voice.channel + await bot.vc_connect_or_move(channel) + await data.reply(f"โœ… Mi sono connesso in [c]#{channel.name}[/c].") + else: + channel_name: str = args[0].lstrip("#") + response = await self.interface.call_herald_action("discord", self._event_name, { + "channel_name": channel_name + }) + await data.reply(f"โœ… Mi sono connesso in [c]#{channel_name}[/c].") diff --git a/royalpack/commands/trivia.py b/royalpack/commands/trivia.py new file mode 100644 index 00000000..b3f637ed --- /dev/null +++ b/royalpack/commands/trivia.py @@ -0,0 +1,139 @@ +import typing +import asyncio +import aiohttp +import random +import uuid +import html +from royalnet.commands import * +from royalnet.utils import asyncify +from ..tables import TriviaScore + + +class TriviaCommand(Command): + name: str = "trivia" + + aliases = ["t"] + + description: str = "Manda una domanda dell'OpenTDB in chat." + + tables = {TriviaScore} + + syntax = "[credits|scores]" + + _letter_emojis = ["๐Ÿ‡ฆ", "๐Ÿ‡ง", "๐Ÿ‡จ", "๐Ÿ‡ฉ"] + + _medal_emojis = ["๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰", "๐Ÿ”น"] + + _correct_emoji = "โœ…" + + _wrong_emoji = "โŒ" + + _answer_time = 17 + + _question_lock: bool = False + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + self._answerers: typing.Dict[uuid.UUID, typing.Dict[str, bool]] = {} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + arg = args.optional(0) + if arg == "credits": + await data.reply(f"โ„น๏ธ [c]{self.interface.prefix}{self.name}[/c] di [i]Steffo[/i]\n" + f"\n" + f"Tutte le domande vengono dall'[b]Open Trivia Database[/b] di [i]Pixeltail Games[/i]," + f" creatori di Tower Unite, e sono rilasciate sotto la licenza [b]CC BY-SA 4.0[/b].") + return + elif arg == "scores": + trivia_scores = await asyncify(data.session.query(self.alchemy.TriviaScore).all) + strings = ["๐Ÿ† [b]Trivia Leaderboards[/b]\n"] + for index, ts in enumerate(sorted(trivia_scores, key=lambda ts: -ts.correct_rate)): + if index > 3: + index = 3 + strings.append(f"{self._medal_emojis[index]} {ts.royal.username}" + f" ({ts.correct_answers}/{ts.total_answers})") + await data.reply("\n".join(strings)) + return + if self._question_lock: + raise CommandError("C'รจ giร  un'altra domanda attiva!") + self._question_lock = True + # Fetch the question + async with aiohttp.ClientSession() as session: + async with session.get("https://opentdb.com/api.php?amount=1") as response: + j = await response.json() + # Parse the question + if j["response_code"] != 0: + raise CommandError(f"OpenTDB returned an error response_code ({j['response_code']}).") + question = j["results"][0] + text = f'โ“ [b]{question["category"]} - {question["difficulty"].capitalize()}[/b]\n' \ + f'{html.unescape(question["question"])}' + # Prepare answers + correct_answer: str = question["correct_answer"] + wrong_answers: typing.List[str] = question["incorrect_answers"] + answers: typing.List[str] = [correct_answer, *wrong_answers] + if question["type"] == "multiple": + random.shuffle(answers) + elif question["type"] == "boolean": + answers.sort(key=lambda a: a) + answers.reverse() + else: + raise NotImplementedError("Unknown question type") + # Find the correct index + for index, answer in enumerate(answers): + if answer == correct_answer: + correct_index = index + break + else: + raise ValueError("correct_index not found") + # Add emojis + for index, answer in enumerate(answers): + answers[index] = f"{self._letter_emojis[index]} {html.unescape(answers[index])}" + # Create the question id + question_id = uuid.uuid4() + self._answerers[question_id] = {} + + # Create the correct and wrong functions + async def correct(data: CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self._answerers[question_id][answerer_.uid] = True + except KeyError: + raise KeyboardExpiredError("Tempo scaduto!") + return "๐Ÿ†— Hai risposto alla domanda. Ora aspetta un attimo per i risultati!" + + async def wrong(data: CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self._answerers[question_id][answerer_.uid] = False + except KeyError: + raise KeyboardExpiredError("Tempo scaduto!") + return "๐Ÿ†— Hai risposto alla domanda. Ora aspetta un attimo per i risultati!" + + # Add question + keyboard = {} + for index, answer in enumerate(answers): + if index == correct_index: + keyboard[answer] = correct + else: + keyboard[answer] = wrong + await data.keyboard(text, keyboard) + await asyncio.sleep(self._answer_time) + results = f"โ—๏ธ Tempo scaduto!\n" \ + f"La risposta corretta era [b]{answers[correct_index]}[/b]!\n\n" + for answerer_id in self._answerers[question_id]: + answerer = data.session.query(self.alchemy.User).get(answerer_id) + if answerer.trivia_score is None: + ts = self.interface.alchemy.TriviaScore(royal=answerer) + data.session.add(ts) + await asyncify(data.session.commit) + if self._answerers[question_id][answerer_id]: + results += self._correct_emoji + answerer.trivia_score.correct_answers += 1 + else: + results += self._wrong_emoji + answerer.trivia_score.wrong_answers += 1 + results += f" {answerer} ({answerer.trivia_score.correct_answers}/{answerer.trivia_score.total_answers})\n" + await data.reply(results) + del self._answerers[question_id] + await asyncify(data.session.commit) + self._question_lock = False diff --git a/royalpack/commands/videochannel.py b/royalpack/commands/videochannel.py new file mode 100644 index 00000000..a09cd17a --- /dev/null +++ b/royalpack/commands/videochannel.py @@ -0,0 +1,50 @@ +import typing +import discord +from royalnet.commands import * + + +class VideochannelCommand(Command): + name: str = "videochannel" + + aliases = ["golive", "live", "video"] + + description: str = "Converti il canale vocale in un canale video." + + syntax = "[channelname]" + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name == "discord": + bot: discord.Client = self.interface.bot + message: discord.Message = data.message + channel_name: str = args.optional(0) + if channel_name: + guild: typing.Optional[discord.Guild] = message.guild + if guild is not None: + channels: typing.List[discord.abc.GuildChannel] = guild.channels + else: + channels = bot.get_all_channels() + matching_channels: typing.List[discord.VoiceChannel] = [] + for channel in channels: + if isinstance(channel, discord.VoiceChannel): + if channel.name == channel_name: + matching_channels.append(channel) + if len(matching_channels) == 0: + raise CommandError("Non esiste alcun canale vocale con il nome specificato.") + elif len(matching_channels) > 1: + raise CommandError("Esiste piรน di un canale vocale con il nome specificato.") + channel = matching_channels[0] + else: + author: discord.Member = message.author + voice: typing.Optional[discord.VoiceState] = author.voice + if voice is None: + raise CommandError("Non sei connesso a nessun canale vocale.") + channel = voice.channel + if author.is_on_mobile(): + await data.reply(f"๐Ÿ“น Per entrare in modalitร  video, clicca qui:\n" + f"\n" + f"[b]Attenzione: la modalitร  video non funziona su Android e iOS![/b]") + return + await data.reply(f"๐Ÿ“น Per entrare in modalitร  video, clicca qui:\n" + f"") + else: + raise UnsupportedError() diff --git a/royalpack/commands/youtube.py b/royalpack/commands/youtube.py new file mode 100644 index 00000000..04f28fec --- /dev/null +++ b/royalpack/commands/youtube.py @@ -0,0 +1,76 @@ +import typing +import pickle +import datetime +import discord +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.audio import YtdlDiscord +from royalnet.bots import DiscordBot + + +class YoutubeCommand(Command): + name: str = "youtube" + + aliases = ["yt"] + + description: str = "Cerca un video su YouTube e lo aggiunge alla coda della chat vocale." + + syntax = "[ [guild] ] {url}" + + @classmethod + async def _legacy_youtube_handler(cls, bot: "DiscordBot", guild_name: typing.Optional[str], search: str): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("Server non trovato.") + if len(guilds) > 1: + raise CommandError("Il nome del server รจ ambiguo.") + guild = list(bot.client.guilds)[0] + # Ensure the guild has a PlayMode before adding the file to it + if not bot.music_data.get(guild): + raise CommandError("Il bot non รจ in nessun canale vocale.") + # Create url + ytdl_args = { + "format": "bestaudio/best", + "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s" + } + # Start downloading + dfiles: typing. List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'ytsearch:{search}', **ytdl_args) + await bot.add_to_music_data(dfiles, guild) + # Create response dictionary + return { + "videos": [{ + "title": dfile.info.title, + "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed())) + } for dfile in dfiles] + } + + _event_name = "_legacy_youtube" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_youtube_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, search = args.match(r"(?:\[(.+)])?\s*?") + if search.startswith("http://") or search.startswith("https://"): + raise CommandError(f"Il comando [c]{self.interface.prefix}youtube[/c] funziona solo per cercare video su" + f" YouTube con un dato nome.\n" + f"Se vuoi riprodurre una canzone da un URL, usa [c]{self.interface.prefix}play[/c]!") + response = await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "search": search + }) + if len(response["videos"]) == 0: + raise CommandError(f"Il video non puรฒ essere scaricato a causa di un blocco imposto da YouTube.") + for video in response["videos"]: + if self.interface.name == "discord": + # This is one of the unsafest things ever + embed = pickle.loads(eval(video["discord_embed_pickle"])) + await data.message.channel.send(content="โ–ถ๏ธ Aggiunto alla coda:", embed=embed) + else: + await data.reply(f"โ–ถ๏ธ Aggiunto alla coda: [i]{video['title']}[/i]") diff --git a/royalpack/commands/zawarudo.py b/royalpack/commands/zawarudo.py new file mode 100644 index 00000000..06f1c946 --- /dev/null +++ b/royalpack/commands/zawarudo.py @@ -0,0 +1,91 @@ +import typing +import discord +import asyncio +import datetime +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.audio import YtdlDiscord +from royalnet.audio.playmodes import Playlist +from royalnet.bots import DiscordBot + + +class ZawarudoCommand(Command): + name: str = "zawarudo" + + aliases = ["theworld", "world"] + + description: str = "Ferma il tempo!" + + syntax = "[ [guild] ] [1-9]" + + @staticmethod + async def _legacy_zawarudo_handler(bot: "DiscordBot", guild_name: typing.Optional[str], time: int): + # Find the matching guild + if guild_name: + guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name) + else: + guilds = bot.client.guilds + if len(guilds) == 0: + raise CommandError("Server non trovato.") + if len(guilds) > 1: + raise CommandError("Il nome del server รจ ambiguo.") + guild = list(bot.client.guilds)[0] + # Ensure the guild has a PlayMode before adding the file to it + if not bot.music_data.get(guild): + raise CommandError("Il bot non รจ in nessun canale vocale.") + # Create url + ytdl_args = { + "format": "bestaudio", + "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s" + } + # Start downloading + zw_start: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, + "https://scaleway.steffo.eu/jojo/zawarudo_intro.mp3", + **ytdl_args) + zw_end: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, + "https://scaleway.steffo.eu/jojo/zawarudo_outro.mp3", + **ytdl_args) + old_playlist = bot.music_data[guild] + bot.music_data[guild].playmode = Playlist() + # Get voice client + vc: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild) + channel: discord.VoiceChannel = vc.channel + affected: typing.List[typing.Union[discord.User, discord.Member]] = channel.members + await bot.add_to_music_data(zw_start, guild) + for member in affected: + if member.bot: + continue + await member.edit(mute=True) + await asyncio.sleep(time) + await bot.add_to_music_data(zw_end, guild) + for member in affected: + member: typing.Union[discord.User, discord.Member] + if member.bot: + continue + await member.edit(mute=False) + bot.music_data[guild] = old_playlist + await bot.advance_music_data(guild) + return {} + + _event_name = "_legacy_zawarudo" + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + if interface.name == "discord": + interface.register_herald_action(self._event_name, self._legacy_zawarudo_handler) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + guild_name, time = args.match(r"(?:\[(.+)])?\s*(.+)?") + if time is None: + time = 5 + else: + time = int(time) + if time < 1: + raise InvalidInputError("The World can't stop time for less than a second.") + if time > 10: + raise InvalidInputError("The World can stop time only for 10 seconds.") + await data.reply(f"๐Ÿ•’ ZA WARUDO! TOKI WO TOMARE!") + await self.interface.call_herald_action("discord", self._event_name, { + "guild_name": guild_name, + "time": time + }) diff --git a/royalpack/stars/__init__.py b/royalpack/stars/__init__.py new file mode 100644 index 00000000..511a9101 --- /dev/null +++ b/royalpack/stars/__init__.py @@ -0,0 +1,21 @@ +# Imports go here! +from .api_user_list import ApiUserListStar +from .api_user_get import ApiUserGetStar +from .api_diario_list import ApiDiarioListStar +from .api_diario_get import ApiDiarioGetStar + +# Enter the PageStars of your Pack here! +available_page_stars = [ + ApiUserListStar, + ApiUserGetStar, + ApiDiarioListStar, + ApiDiarioGetStar, +] + +# Enter the ExceptionStars of your Pack here! +available_exception_stars = [ + +] + +# Don't change this, it should automatically generate __all__ +__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]] diff --git a/royalpack/stars/api_diario_get.py b/royalpack/stars/api_diario_get.py new file mode 100644 index 00000000..5c2ddf53 --- /dev/null +++ b/royalpack/stars/api_diario_get.py @@ -0,0 +1,22 @@ +from starlette.requests import Request +from starlette.responses import * +from royalnet.web import * +from royalnet.utils import * +from ..tables import Diario + + +class ApiDiarioGetStar(PageStar): + path = "/api/diario/get/{diario_id}" + tables = {Diario} + + async def page(self, request: Request) -> JSONResponse: + diario_id_str = request.path_params.get("diario_id", "") + try: + diario_id = int(diario_id_str) + except (ValueError, TypeError): + return error(400, "Invalid diario_id") + async with self.alchemy.session_acm() as session: + entry: Diario = await asyncify(session.query(self.alchemy.User).get, diario_id) + if entry is None: + return error(404, "No such user") + return JSONResponse(entry.json()) diff --git a/royalpack/stars/api_diario_list.py b/royalpack/stars/api_diario_list.py new file mode 100644 index 00000000..0b11af91 --- /dev/null +++ b/royalpack/stars/api_diario_list.py @@ -0,0 +1,25 @@ +from starlette.requests import Request +from starlette.responses import * +from royalnet.web import * +from royalnet.utils import * +from ..tables import Diario + + +class ApiDiarioListStar(PageStar): + path = "/api/diario/list" + tables = {Diario} + + async def page(self, request: Request) -> JSONResponse: + page_str = request.query_params.get("page", "0") + try: + page = int(page_str) + except (ValueError, TypeError): + return error(400, "Invalid offset") + async with self.alchemy.session_acm() as session: + if page < 0: + page = -page-1 + entries: typing.List[Diario] = await asyncify(session.query(self.alchemy.Diario).order_by(self.alchemy.Diario.diario_id.desc()).limit(500).offset(page * 500).all) + else: + entries: typing.List[Diario] = await asyncify(session.query(self.alchemy.Diario).order_by(self.alchemy.Diario.diario_id).limit(500).offset(page * 500).all) + response = [entry.json() for entry in entries] + return JSONResponse(response) diff --git a/royalpack/stars/api_user_get.py b/royalpack/stars/api_user_get.py new file mode 100644 index 00000000..d547c7b6 --- /dev/null +++ b/royalpack/stars/api_user_get.py @@ -0,0 +1,22 @@ +from starlette.requests import Request +from starlette.responses import * +from royalnet.web import * +from royalnet.utils import * +from royalnet.packs.common.tables import User + + +class ApiUserGetStar(PageStar): + path = "/api/user/get/{uid_str}" + tables = {User} + + async def page(self, request: Request) -> JSONResponse: + uid_str = request.path_params.get("uid_str", "") + try: + uid = int(uid_str) + except (ValueError, TypeError): + return error(400, "Invalid uid") + async with self.alchemy.session_acm() as session: + user: User = await asyncify(session.query(self.alchemy.User).get, uid) + if user is None: + return error(404, "No such user") + return JSONResponse(user.json()) diff --git a/royalpack/stars/api_user_list.py b/royalpack/stars/api_user_list.py new file mode 100644 index 00000000..3b9a562e --- /dev/null +++ b/royalpack/stars/api_user_list.py @@ -0,0 +1,15 @@ +from starlette.requests import Request +from starlette.responses import * +from royalnet.web import * +from royalnet.utils import * +from royalnet.packs.common.tables import User + + +class ApiUserListStar(PageStar): + path = "/api/user/list" + tables = {User} + + async def page(self, request: Request) -> JSONResponse: + async with self.alchemy.session_acm() as session: + users: typing.List[User] = await asyncify(session.query(self.alchemy.User).all) + return JSONResponse([user.json() for user in users]) diff --git a/royalpack/tables/__init__.py b/royalpack/tables/__init__.py new file mode 100644 index 00000000..30ce6f79 --- /dev/null +++ b/royalpack/tables/__init__.py @@ -0,0 +1,35 @@ +# Imports go here! +from royalnet.packs.common.tables import User +from royalnet.packs.common.tables import Telegram +from royalnet.packs.common.tables import Discord + +from .diario import Diario +from .aliases import Alias +from .wikipages import WikiPage +from .wikirevisions import WikiRevision +from .bios import Bio +from .reminders import Reminder +from .triviascores import TriviaScore +from .mmevents import MMEvent +from .mmresponse import MMResponse +from .leagueoflegends import LeagueOfLegends + +# Enter the tables of your Pack here! +available_tables = [ + User, + Telegram, + Discord, + Diario, + Alias, + WikiPage, + WikiRevision, + Bio, + Reminder, + TriviaScore, + MMEvent, + MMResponse, + LeagueOfLegends +] + +# Don't change this, it should automatically generate __all__ +__all__ = [table.__name__ for table in available_tables] diff --git a/royalpack/tables/aliases.py b/royalpack/tables/aliases.py new file mode 100644 index 00000000..2a1c99ec --- /dev/null +++ b/royalpack/tables/aliases.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, \ + Integer, \ + String, \ + ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr + + +class Alias: + __tablename__ = "aliases" + + @declared_attr + def royal_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def alias(self): + return Column(String, primary_key=True) + + @declared_attr + def royal(self): + return relationship("User", backref="aliases") + + def __repr__(self): + return f"" + + def __str__(self): + return f"{self.alias}->{self.royal_id}" diff --git a/royalpack/tables/bios.py b/royalpack/tables/bios.py new file mode 100644 index 00000000..25a3a630 --- /dev/null +++ b/royalpack/tables/bios.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, \ + Integer, \ + Text, \ + ForeignKey +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declared_attr + + +class Bio: + __tablename__ = "bios" + + @declared_attr + def royal_id(self): + return Column(Integer, ForeignKey("users.uid"), primary_key=True) + + @declared_attr + def royal(self): + return relationship("User", backref=backref("bio", uselist=False)) + + @declared_attr + def contents(self): + return Column(Text, nullable=False, default="") + + def __repr__(self): + return f"" + + def __str__(self): + return self.contents diff --git a/royalpack/tables/diario.py b/royalpack/tables/diario.py new file mode 100644 index 00000000..32769861 --- /dev/null +++ b/royalpack/tables/diario.py @@ -0,0 +1,105 @@ +import re +import datetime +from sqlalchemy import Column, \ + Integer, \ + Text, \ + Boolean, \ + DateTime, \ + ForeignKey, \ + String +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr + + +class Diario: + __tablename__ = "diario" + + @declared_attr + def diario_id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def creator_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def quoted_account_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def quoted(self): + return Column(String) + + @declared_attr + def text(self): + return Column(Text) + + @declared_attr + def context(self): + return Column(Text) + + @declared_attr + def timestamp(self) -> datetime.datetime: + return Column(DateTime, nullable=False) + + @declared_attr + def media_url(self): + return Column(String) + + @declared_attr + def spoiler(self): + return Column(Boolean, default=False) + + @declared_attr + def creator(self): + return relationship("User", foreign_keys=self.creator_id, backref="diario_created") + + @declared_attr + def quoted_account(self): + return relationship("User", foreign_keys=self.quoted_account_id, backref="diario_quoted") + + def json(self) -> dict: + return { + "diario_id": self.diario_id, + "creator": self.creator.json() if self.creator else None, + "quoted_account": self.quoted_account.json() if self.quoted_account else None, + "quoted": self.quoted, + "text": self.text, + "context": self.context, + "timestamp": self.timestamp.isoformat(), + "media_url": self.media_url, + "spoiler": self.spoiler + } + + def __repr__(self): + return f"" + + def __str__(self): + text = f"Riga #{self.diario_id}" + text += f" (salvata da {str(self.creator)}" + text += f" il {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n" + if self.media_url is not None: + text += f"{self.media_url}\n" + if self.text is not None: + if self.spoiler: + hidden = re.sub(r"\w", "โ–ˆ", self.text) + text += f"\"{hidden}\"\n" + else: + text += f"[b]\"{self.text}\"[/b]\n" + if self.quoted_account is not None: + text += f" โ€”{str(self.quoted_account)}" + elif self.quoted is not None: + text += f" โ€”{self.quoted}" + else: + text += f" โ€”Anonimo" + if self.context: + text += f", [i]{self.context}[/i]" + return text diff --git a/royalpack/tables/leagueoflegends.py b/royalpack/tables/leagueoflegends.py new file mode 100644 index 00000000..87763fc3 --- /dev/null +++ b/royalpack/tables/leagueoflegends.py @@ -0,0 +1,256 @@ +from sqlalchemy import * +from sqlalchemy.orm import relationship, composite +from sqlalchemy.ext.declarative import declared_attr +from ..utils import LeagueRank, LeagueTier, LeagueLeague + + +class LeagueOfLegends: + __tablename__ = "leagueoflegends" + + @declared_attr + def region(self): + return Column(String, nullable=False) + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def user(self): + return relationship("User", backref="leagueoflegends") + + @declared_attr + def profile_icon_id(self): + # 3777 + return Column(Integer, nullable=False) + + @declared_attr + def summoner_name(self): + # SteffoRYG + return Column(String, nullable=False) + + @declared_attr + def puuid(self): + # iNW0i7w_cC2kxgNB13UhyGPeyxZChmRqKylZ--bzbZAhFM6EXAImUqeRWmGtK6iKiYbz3bkCV8fMQQ + return Column(String, nullable=False) + + @declared_attr + def summoner_level(self): + # 68 + return Column(Integer, nullable=False) + + @declared_attr + def summoner_id(self): + # aEsHyfXA2q8bK-g7GlT4kFK_0uLL3w-jBPyfMAy8kOXTJXo + return Column(String, nullable=False, primary_key=True) + + @declared_attr + def account_id(self): + # -2Ex-VpkkNBN4ceQev8oJsamxY5iGb2liRUqkES5TU_7vtI + return Column(String, nullable=False) + + @declared_attr + def rank_soloq_tier(self): + return Column(Enum(LeagueTier)) + + @declared_attr + def rank_soloq_rank(self): + return Column(Enum(LeagueRank)) + + @declared_attr + def rank_soloq_points(self): + return Column(Integer) + + @declared_attr + def rank_soloq_wins(self): + return Column(Integer) + + @declared_attr + def rank_soloq_losses(self): + return Column(Integer) + + @declared_attr + def rank_soloq_inactive(self): + return Column(Boolean) + + @declared_attr + def rank_soloq_hot_streak(self): + return Column(Boolean) + + @declared_attr + def rank_soloq_fresh_blood(self): + return Column(Boolean) + + @declared_attr + def rank_soloq_veteran(self): + return Column(Boolean) + + @declared_attr + def rank_soloq(self): + return composite(LeagueLeague, + self.rank_soloq_tier, + self.rank_soloq_rank, + self.rank_soloq_points, + self.rank_soloq_wins, + self.rank_soloq_losses, + self.rank_soloq_inactive, + self.rank_soloq_hot_streak, + self.rank_soloq_fresh_blood, + self.rank_soloq_veteran) + + @declared_attr + def rank_flexq_tier(self): + return Column(Enum(LeagueTier)) + + @declared_attr + def rank_flexq_rank(self): + return Column(Enum(LeagueRank)) + + @declared_attr + def rank_flexq_points(self): + return Column(Integer) + + @declared_attr + def rank_flexq_wins(self): + return Column(Integer) + + @declared_attr + def rank_flexq_losses(self): + return Column(Integer) + + @declared_attr + def rank_flexq_inactive(self): + return Column(Boolean) + + @declared_attr + def rank_flexq_hot_streak(self): + return Column(Boolean) + + @declared_attr + def rank_flexq_fresh_blood(self): + return Column(Boolean) + + @declared_attr + def rank_flexq_veteran(self): + return Column(Boolean) + + @declared_attr + def rank_flexq(self): + return composite(LeagueLeague, + self.rank_flexq_tier, + self.rank_flexq_rank, + self.rank_flexq_points, + self.rank_flexq_wins, + self.rank_flexq_losses, + self.rank_flexq_inactive, + self.rank_flexq_hot_streak, + self.rank_flexq_fresh_blood, + self.rank_flexq_veteran) + + @declared_attr + def rank_twtrq_tier(self): + return Column(Enum(LeagueTier)) + + @declared_attr + def rank_twtrq_rank(self): + return Column(Enum(LeagueRank)) + + @declared_attr + def rank_twtrq_points(self): + return Column(Integer) + + @declared_attr + def rank_twtrq_wins(self): + return Column(Integer) + + @declared_attr + def rank_twtrq_losses(self): + return Column(Integer) + + @declared_attr + def rank_twtrq_inactive(self): + return Column(Boolean) + + @declared_attr + def rank_twtrq_hot_streak(self): + return Column(Boolean) + + @declared_attr + def rank_twtrq_fresh_blood(self): + return Column(Boolean) + + @declared_attr + def rank_twtrq_veteran(self): + return Column(Boolean) + + @declared_attr + def rank_twtrq(self): + return composite(LeagueLeague, + self.rank_twtrq_tier, + self.rank_twtrq_rank, + self.rank_twtrq_points, + self.rank_twtrq_wins, + self.rank_twtrq_losses, + self.rank_twtrq_inactive, + self.rank_twtrq_hot_streak, + self.rank_twtrq_fresh_blood, + self.rank_twtrq_veteran) + + @declared_attr + def rank_tftq_tier(self): + return Column(Enum(LeagueTier)) + + @declared_attr + def rank_tftq_rank(self): + return Column(Enum(LeagueRank)) + + @declared_attr + def rank_tftq_points(self): + return Column(Integer) + + @declared_attr + def rank_tftq_wins(self): + return Column(Integer) + + @declared_attr + def rank_tftq_losses(self): + return Column(Integer) + + @declared_attr + def rank_tftq_inactive(self): + return Column(Boolean) + + @declared_attr + def rank_tftq_hot_streak(self): + return Column(Boolean) + + @declared_attr + def rank_tftq_fresh_blood(self): + return Column(Boolean) + + @declared_attr + def rank_tftq_veteran(self): + return Column(Boolean) + + @declared_attr + def rank_tftq(self): + return composite(LeagueLeague, + self.rank_tftq_tier, + self.rank_tftq_rank, + self.rank_tftq_points, + self.rank_tftq_wins, + self.rank_tftq_losses, + self.rank_tftq_inactive, + self.rank_tftq_hot_streak, + self.rank_tftq_fresh_blood, + self.rank_tftq_veteran) + + @declared_attr + def mastery_score(self): + return Column(Integer, nullable=False, default=0) + + def __repr__(self): + return f"<{self.__class__.__qualname__} {str(self)}>" + + def __str__(self): + return f"[c]{self.__tablename__}:{self.summoner_name}[/c]" diff --git a/royalpack/tables/mmevents.py b/royalpack/tables/mmevents.py new file mode 100644 index 00000000..f633c0f8 --- /dev/null +++ b/royalpack/tables/mmevents.py @@ -0,0 +1,52 @@ +import pickle +from sqlalchemy import * +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr + + +class MMEvent: + __tablename__ = "mmevents" + + @declared_attr + def creator_id(self): + return Column(Integer, ForeignKey("users.uid"), nullable=False) + + @declared_attr + def creator(self): + return relationship("User", backref="mmevents_created") + + @declared_attr + def mmid(self): + return Column(Integer, primary_key=True) + + @declared_attr + def datetime(self): + return Column(DateTime, nullable=False) + + @declared_attr + def title(self): + return Column(String, nullable=False) + + @declared_attr + def description(self): + return Column(Text, nullable=False, default="") + + @declared_attr + def interface(self): + return Column(String, nullable=False) + + @declared_attr + def raw_interface_data(self): + # The default is a pickled None + return Column(Binary, nullable=False, default=b'\x80\x03N.') + + @property + def interface_data(self): + return pickle.loads(self.raw_interface_data) + + @interface_data.setter + def interface_data(self, value): + self.raw_interface_data = pickle.dumps(value) + + def __repr__(self): + return f"" diff --git a/royalpack/tables/mmresponse.py b/royalpack/tables/mmresponse.py new file mode 100644 index 00000000..4d04931a --- /dev/null +++ b/royalpack/tables/mmresponse.py @@ -0,0 +1,31 @@ +from sqlalchemy import * +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr +from ..utils import MMChoice + + +class MMResponse: + __tablename__ = "mmresponse" + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid"), primary_key=True) + + @declared_attr + def user(self): + return relationship("User", backref="mmresponses_given") + + @declared_attr + def mmevent_id(self): + return Column(Integer, ForeignKey("mmevents.mmid"), primary_key=True) + + @declared_attr + def mmevent(self): + return relationship("MMEvent", backref="responses") + + @declared_attr + def choice(self): + return Column(Enum(MMChoice), nullable=False) + + def __repr__(self): + return f"" diff --git a/royalpack/tables/reminders.py b/royalpack/tables/reminders.py new file mode 100644 index 00000000..17f94302 --- /dev/null +++ b/royalpack/tables/reminders.py @@ -0,0 +1,46 @@ +from sqlalchemy import Column, \ + Integer, \ + String, \ + LargeBinary, \ + DateTime, \ + ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr + + +class Reminder: + __tablename__ = "reminder" + + @declared_attr + def reminder_id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def creator_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def creator(self): + return relationship("User", backref="reminders_created") + + @declared_attr + def interface_name(self): + return Column(String) + + @declared_attr + def interface_data(self): + return Column(LargeBinary) + + @declared_attr + def datetime(self): + return Column(DateTime) + + @declared_attr + def message(self): + return Column(String) + + def __repr__(self): + return f"" + + def __str__(self): + return self.message diff --git a/royalpack/tables/triviascores.py b/royalpack/tables/triviascores.py new file mode 100644 index 00000000..39d251ae --- /dev/null +++ b/royalpack/tables/triviascores.py @@ -0,0 +1,40 @@ +from sqlalchemy import Column, \ + Integer, \ + ForeignKey +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declared_attr + + +class TriviaScore: + __tablename__ = "triviascores" + + @declared_attr + def royal_id(self): + return Column(Integer, ForeignKey("users.uid"), primary_key=True) + + @declared_attr + def royal(self): + return relationship("User", backref=backref("trivia_score", uselist=False)) + + @declared_attr + def correct_answers(self): + return Column(Integer, nullable=False, default=0) + + @declared_attr + def wrong_answers(self): + return Column(Integer, nullable=False, default=0) + + @property + def total_answers(self): + return self.correct_answers + self.wrong_answers + + @property + def offset(self): + return self.correct_answers - self.wrong_answers + + @property + def correct_rate(self): + return self.correct_answers / self.total_answers + + def __repr__(self): + return f"" diff --git a/royalpack/tables/wikipages.py b/royalpack/tables/wikipages.py new file mode 100644 index 00000000..91719fc8 --- /dev/null +++ b/royalpack/tables/wikipages.py @@ -0,0 +1,38 @@ +from sqlalchemy import Column, \ + Text, \ + String +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declared_attr +from royalnet.utils import to_urluuid + + +class WikiPage: + """Wiki page properties. + + Warning: + Requires PostgreSQL!""" + __tablename__ = "wikipages" + + @declared_attr + def page_id(self): + return Column(UUID(as_uuid=True), primary_key=True) + + @declared_attr + def title(self): + return Column(String, nullable=False) + + @declared_attr + def contents(self): + return Column(Text) + + @declared_attr + def format(self): + return Column(String, nullable=False, default="markdown") + + @declared_attr + def css(self): + return Column(String) + + @property + def page_short_id(self): + return to_urluuid(self.page_id) diff --git a/royalpack/tables/wikirevisions.py b/royalpack/tables/wikirevisions.py new file mode 100644 index 00000000..456376d7 --- /dev/null +++ b/royalpack/tables/wikirevisions.py @@ -0,0 +1,48 @@ +from sqlalchemy import Column, \ + Integer, \ + Text, \ + DateTime, \ + ForeignKey +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declared_attr + + +class WikiRevision: + """A wiki page revision. + + Warning: + Requires PostgreSQL!""" + __tablename__ = "wikirevisions" + + @declared_attr + def revision_id(self): + return Column(UUID(as_uuid=True), primary_key=True) + + @declared_attr + def page_id(self): + return Column(UUID(as_uuid=True), ForeignKey("wikipages.page_id"), nullable=False) + + @declared_attr + def page(self): + return relationship("WikiPage", foreign_keys=self.page_id, backref="revisions") + + @declared_attr + def author_id(self): + return Column(Integer, ForeignKey("users.uid"), nullable=False) + + @declared_attr + def author(self): + return relationship("User", foreign_keys=self.author_id, backref="wiki_contributions") + + @declared_attr + def timestamp(self): + return Column(DateTime, nullable=False) + + @declared_attr + def reason(self): + return Column(Text) + + @declared_attr + def diff(self): + return Column(Text) diff --git a/royalpack/utils/__init__.py b/royalpack/utils/__init__.py new file mode 100644 index 00000000..aaab3673 --- /dev/null +++ b/royalpack/utils/__init__.py @@ -0,0 +1,7 @@ +from .mmchoice import MMChoice +from .mminterfacedata import MMInterfaceData, MMInterfaceDataTelegram +from .leaguetier import LeagueTier +from .leaguerank import LeagueRank +from .leagueleague import LeagueLeague + +__all__ = ["MMChoice", "MMInterfaceData", "MMInterfaceDataTelegram", "LeagueTier", "LeagueRank", "LeagueLeague"] diff --git a/royalpack/utils/leagueleague.py b/royalpack/utils/leagueleague.py new file mode 100644 index 00000000..96d17523 --- /dev/null +++ b/royalpack/utils/leagueleague.py @@ -0,0 +1,134 @@ +from .leaguetier import LeagueTier +from .leaguerank import LeagueRank + + +class LeagueLeague: + def __init__(self, + tier: LeagueTier = None, + rank: LeagueRank = None, + points: int = None, + wins: int = None, + losses: int = None, + inactive: bool = None, + hot_streak: bool = None, + fresh_blood: bool = None, + veteran: bool = None): + self.tier: LeagueTier = tier # IRON + self.rank: LeagueRank = rank # I + self.points: int = points # 40 LP + self.wins: int = wins + self.losses: int = losses + self.inactive: bool = inactive + self.hot_streak: bool = hot_streak + self.fresh_blood: bool = fresh_blood + self.veteran: bool = veteran + + def __str__(self) -> str: + emojis = "" + if self.veteran: + emojis += "๐Ÿ†" + if self.hot_streak: + emojis += "๐Ÿ”ฅ" + if self.fresh_blood: + emojis += "โญ๏ธ" + return f"[b]{self.tier} {self.rank}[/b] ({self.points} LP){' ' if emojis else ''}{emojis}" + + def __repr__(self) -> str: + return f"<{self.__class__.__qualname__} {self}>" + + def __eq__(self, other) -> bool: + if other is None: + return False + if not isinstance(other, LeagueLeague): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + equal = True + if other.veteran: + equal &= self.veteran == other.veteran + if other.fresh_blood: + equal &= self.fresh_blood == other.fresh_blood + if other.hot_streak: + equal &= self.hot_streak == other.hot_streak + if other.inactive: + equal &= self.inactive == other.inactive + if other.losses: + equal &= self.losses == other.losses + if other.wins: + equal &= self.wins == other.wins + if other.points: + equal &= self.points == other.points + if other.rank: + equal &= self.rank == other.rank + if other.tier: + equal &= self.tier == other.tier + return equal + + def __ne__(self, other) -> bool: + return not self.__eq__(other) + + def __gt__(self, other) -> bool: + if other is None: + return True + if not isinstance(other, LeagueLeague): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + if not (bool(self) and bool(other)): + raise ValueError("Can't compare partial LeagueLeagues.") + if self.tier != other.tier: + # Silver is better than Bronze + return self.tier > other.tier + elif self.rank != other.rank: + # Silver I is better than Silver IV + return self.rank > other.rank + elif self.points != other.points: + # Silver I (100 LP) is better than Silver I (0 LP) + return self.points > other.points + elif self.winrate != other.winrate: + # Silver I (100 LP with 60% winrate) is better than Silver I (100 LP with 40% winrate) + return self.winrate > other.winrate + else: + return False + + def __bool__(self): + result = True + result &= self.veteran is not None + result &= self.fresh_blood is not None + result &= self.hot_streak is not None + result &= self.inactive is not None + result &= self.losses is not None + result &= self.wins is not None + result &= self.points is not None + result &= self.rank is not None + result &= self.tier is not None + return result + + def __composite_values__(self): + return self.tier, \ + self.rank, \ + self.points, \ + self.wins, \ + self.losses, \ + self.inactive, \ + self.hot_streak, \ + self.fresh_blood, \ + self.veteran + + @property + def played(self): + return self.wins + self.losses + + @property + def winrate(self): + return self.wins / self.played + + @classmethod + def from_dict(cls, d: dict): + return cls( + tier=LeagueTier.from_string(d["tier"]), + rank=LeagueRank.from_string(d["rank"]), + points=d["leaguePoints"], + wins=d["wins"], + losses=d["losses"], + inactive=d["inactive"], + hot_streak=d["hotStreak"], + fresh_blood=d["freshBlood"], + veteran=d["veteran"], + ) diff --git a/royalpack/utils/leaguerank.py b/royalpack/utils/leaguerank.py new file mode 100644 index 00000000..ede917d7 --- /dev/null +++ b/royalpack/utils/leaguerank.py @@ -0,0 +1,21 @@ +import enum + + +class LeagueRank(enum.Enum): + I = 1 + II = 2 + III = 3 + IV = 4 + + def __str__(self): + return self.name + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + return self.value < other.value + + @classmethod + def from_string(cls, string: str): + return cls.__members__.get(string) diff --git a/royalpack/utils/leaguetier.py b/royalpack/utils/leaguetier.py new file mode 100644 index 00000000..011b290a --- /dev/null +++ b/royalpack/utils/leaguetier.py @@ -0,0 +1,26 @@ +import enum + + +class LeagueTier(enum.Enum): + IRON = 0 + BRONZE = 1 + SILVER = 2 + GOLD = 3 + PLATINUM = 4 + DIAMOND = 5 + MASTER = 6 + GRANDMASTER = 7 + CHALLENGER = 8 + + def __str__(self): + return self.name.capitalize() + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + return self.value > other.value + + @classmethod + def from_string(cls, string: str): + return cls.__members__.get(string) diff --git a/royalpack/utils/mmchoice.py b/royalpack/utils/mmchoice.py new file mode 100644 index 00000000..5f54a507 --- /dev/null +++ b/royalpack/utils/mmchoice.py @@ -0,0 +1,12 @@ +import enum + + +class MMChoice(enum.Enum): + YES = "๐Ÿ”ต" + MAYBE = "โ”" + LATE_SHORT = "๐Ÿ•" + LATE_MEDIUM = "๐Ÿ•’" + LATE_LONG = "๐Ÿ•—" + NO_TIME = "๐Ÿ”ด" + NO_INTEREST = "โŒ" + NO_TECH = "โ—๏ธ" diff --git a/royalpack/utils/mminterfacedata.py b/royalpack/utils/mminterfacedata.py new file mode 100644 index 00000000..4cfd6d32 --- /dev/null +++ b/royalpack/utils/mminterfacedata.py @@ -0,0 +1,10 @@ +class MMInterfaceData: + def __init__(self): + pass + + +class MMInterfaceDataTelegram(MMInterfaceData): + def __init__(self, chat_id: int, message_id: int): + super().__init__() + self.chat_id = chat_id + self.message_id = message_id diff --git a/royalpack/version.py b/royalpack/version.py new file mode 100644 index 00000000..9d42775b --- /dev/null +++ b/royalpack/version.py @@ -0,0 +1,4 @@ +semantic = "5.0a92" + +if __name__ == "__main__": + print(semantic) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..c70cb813 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +import royalpack.version +import setuptools + +with open("README.md", "r") as f: + long_description = f.read() + +with open("requirements.txt", "r") as f: + install_requires = [line for line in f.readlines() if not line.startswith("#")] + +setuptools.setup( + name="royalpack", + version=royalpack.version.semantic, + author="Stefano Pigozzi", + author_email="ste.pigozzi@gmail.com", + description="A Royalnet Pack for the Royal Games community", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Steffo99/royalpack", + packages=setuptools.find_packages(), + install_requires=install_requires, + python_requires=">=3.7", + classifiers=[ + "Development Status :: 3 - Alpha", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.7", + "Topic :: Internet", + "Topic :: Database", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Video", + "License :: OSI Approved :: MIT License" + ], + dependency_links=["https://github.com/Rapptz/discord.py/tarball/master"], + include_package_data=True, + zip_safe=False +) diff --git a/to_pypi.bat b/to_pypi.bat new file mode 100644 index 00000000..40941be4 --- /dev/null +++ b/to_pypi.bat @@ -0,0 +1,4 @@ + +del /f /q /s dist\*.* +python setup.py sdist bdist_wheel +twine upload dist/* diff --git a/to_pypi.sh b/to_pypi.sh new file mode 100755 index 00000000..7a431000 --- /dev/null +++ b/to_pypi.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Royalnet must be installed with `develop` +VERSION=$(python3.7 -m royalnet.version) + +rm -rf dist +python setup.py sdist bdist_wheel +twine upload "dist/royalnet-$VERSION"* +git add * +git commit -m "$VERSION" +git push +hub release create --message "Royalnet $VERSION" --prerelease "$VERSION"